I'm trying to learn React (with redux) so i'm making an app where i'm able to create workout plans, add workouts to them and then add exercises to a workout.
PlanListComponent
import { Button, Card, Typography } from "#material-ui/core/";
import { makeStyles } from "#material-ui/core/styles";
import DeleteIcon from "#material-ui/icons/Delete";
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { deletePlan, getPlans } from "../actions/plansActions";
import AddWorkouts from "./AddWorkouts";
const useStyles = makeStyles((theme) => ({}));
function PlansList() {
const classes = useStyles();
const { plans } = useSelector((state) => state.plans);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getPlans());
}, [dispatch]);
return (
<div>
{plans.map((plan) => (
<Card key={plan._id}>
<Typography>{plan.name}</Typography>
<Typography>{plan._id}</Typography>
<div>
{plan.workouts.map((workout) => (
<li key={workout._id}>{workout.description}</li>
))}
</div>
<AddWorkouts plan={plan} />
<Button onClick={() => dispatch(deletePlan(plan._id))}>
<DeleteIcon /> Delete
</Button>
</Card>
))}
</div>
);
}
export default PlansList;
My PlansList component renders a card for every plan. Within this card it renders a list of workouts for every workout within that plan. After adding a workout to a plan, the PlansList component does not rerender. The added workout only shows after i refresh the page. I am guesing this happens because i have to update the state of the nested workout array in order to make React rerender my component.
These are my actions and reducers for adding a workout to a plan. The payload i am sending in my action is an array of objects.
Action
export const addWorkouts = (workouts, planId) => (dispatch, getState) => {
axios
.post(`/workouts/${planId}`, workouts, tokenConfig(getState))
.then(res =>
dispatch({
type: ADD_WORKOUTS,
id: planId
payload: res.data
}));
}
Reducer
const initialState = {
plans: [{
workouts: []
}],
isLoading: false,
};
export default function (state = initialState, action) {
switch (action.type) {
case ADD_WORKOUTS:
return {
...state,
plans: {
// guessing i should find the right plan by id here
...state.plans,
workouts: {
...state.plans.workouts,
workouts: state.plans.workouts.concat(action.payload)
}
}
};
default:
return state;
}
I've seen a lot of tutorials on how to update the state of nested arrays and tried a few different things, but i can't seem to find the right solution here.
Does anyone has any idea how to fix this issue?
Your reducer is not updating the right things. You're actually converting the property workouts which was an array into an object which has a property workouts which is an array. It's not what you want to do at all.
Finding and updating an array element in redux is not work the bother when it's easy to have plans be an object which is keyed by the plan id. Does the order of the plans matter? If it does, I would still keep the plans keyed by id, but I would have a separate value in the state which stores an ordered array of plan ids.
What I am suggesting looks like this:
const initialState = {
plans: {}, // you could call this plansById
isLoading: false,
planOrder: [] // totally optional and only needed if the order matters when selecting all plans
};
export default function (state = initialState, action) {
switch (action.type) {
case ADD_WORKOUTS:
const existingPlan = state.plans[action.id] || {};
return {
...state,
plans: {
...state.plans,
[action.id]: {
...existingPlan,
workouts: ( existingPlan.workouts || [] ).concat(action.payload)
}
}
};
default:
return state;
}
You can break the reducer up into smaller pieces. If you have multiple actions which update a plan, you can make a reducer which handles an individual plan so that you don't have to repeat all of the ... stuff more than once.
function planReducer (planState = { workouts: []}, action ) {
switch (action.type) {
case ADD_WORKOUTS:
return {
...planState,
workouts: planState.workouts.concat(action.payload)
};
default:
return planState;
}
}
function rootReducer (state = initialState, action) {
/**
* you can use a `switch` statement and list all actions which modify an individual plan
* or use an `if` statement and check for something, like if the action has a `planId` property
*/
switch (action.type) {
case ADD_WORKOUTS:
case OTHER_ACTION:
return {
...state,
plans: {
...state.plans,
[action.id]: planReducer(state.plans[action.id], action),
}
};
default:
return state;
}
}
With the suggested reducers, you don't need to make any changes to your action, but you do need to change your selector because plans is no longer an array. Your selector becomes (state) => Object.values(state.plans).
If you stored a specific plan order, you would select the order and map from the order to the individual plans: (state) => state.planOrder.map( id => state.plans[id] ).
Related
I am new to using "#reduxjs/toolkit" (version "^1.5.1").
I am trying to remove an object from within the state's array (roundScore). This is usually something that is very simple to do using filter(). For some reason this isn't working and I can't figure out why. Here's my code:
Reducer slice:
import { createSlice } from "#reduxjs/toolkit";
export const roundScoreSlice = createSlice({
name: "roundScore",
initialState: {
roundScore: [],
},
reducers: {
deleteArrow: (state, action) => {
console.log(`action.payload = ${action.payload}`); // returns correct id
state.roundScore.filter((arrow) => arrow.id !== action.payload);
},
},
});
export const { deleteArrow } = roundScoreSlice.actions;
export default roundScoreSlice.reducer;
React component:
import React from "react";
import styled from "styled-components";
import { motion } from "framer-motion";
import { useDispatch } from "react-redux";
import { deleteArrow } from "../../redux-reducers/trackSession/roundScoreSlice";
export default function InputtedScore({
arrowScore,
id,
initial,
animate,
variants,
}) {
const dispatch = useDispatch();
const applyStyling = () => {
switch (arrowScore) {
case 0:
return "miss";
case 1:
return "white";
case 2:
return "white";
case 3:
return "black";
case 4:
return "black";
case 5:
return "blue";
case 6:
return "blue";
case 7:
return "red";
case 8:
return "red";
case 9:
return "gold";
case 10:
return "gold";
default:
return null;
}
};
return (
<ParentStyled
id={id}
initial={initial}
animate={animate}
variants={variants}
onClick={() => dispatch(deleteArrow(id))}
>
<Circle className={applyStyling()}>
{arrowScore}
<IconStyled>
<IoIosClose />
</IconStyled>
<IoIosClose className="redCross" />
</Circle>
</ParentStyled>
);
}
The state after adding 2 arrows would look like this:
roundScore: [
{
id:"e0f225ba-19c2-4fd4-b2bf-1e0aef6ab4e0"
arrowScore:7
},
{
id:"2218385f-b37a-4f2c-a8db-4e7e65846171"
arrowScore:5
}
]
I've tried a combination of things.
Using e.target.id within dispatch
Using e.currentTarget.id within dispatch
Using ({id}) instead of just (id) within dispatch
Wrapping the reducer function with or without braces e.g. within (state, action) => { /* code */ }
What is it I'm missing? I know this is going to be a simple fix but for some reason it's eluding me.
Any help is much appreciated.
Okay, it looks like the issue is in the way how filter method works, it returns a new array, and an initial array is not mutated (that's why we have been using filter before to manipulate redux state), also in the code you've shown value of the filtering operation not assigned to any property of your state
You need to assign the value or mutate array, so the code below should work for you
state.roundScore = state.roundScore.filter((arrow) => arrow.id !== action.payload);
Mutate your existing array:
state.roundScore.splice(state.roundScore.findIndex((arrow) => arrow.id === action.payload), 1);
We can think outside of the box and look at it from another way. the React component above is just actually a child of a certain parent component. And for the purpose of my answer i assume that in your parent component you have some form of array.map .
So from that code, each array item will already have an array index. and you can pass that index as a prop to the above react component like so:
const InputtedScore = ({ ...all your props, id, inputIndex }) => {
const dispatch = useDispatch();
// Having access to your index from the props, you can even
// find the item corresponding to that index
const inputAtIndex_ = useSelector(state => state.input[inputIndex])
const applyStyling = () => {
switch (arrowScore) {
...your style logic
}
};
return (
// you can send that index as a payload to the reducer function
<ParentStyled id={id} onClick={() => dispatch(deleteArrow(inputIndex))}
...the rest of your properties >
<Circle className={applyStyling()}>
{arrowScore}
<IconStyled>
<IoIosClose />
</IconStyled>
<IoIosClose className="redCross" />
</Circle>
</ParentStyled>
);
}
After dispaching the delete action by sending as payload the item's index already, you do not need to find the item in the reducer anymore:
deleteMeal: (state, action) => {
// you receive you inputIndex from the payload
let { inputIndex } = action.payload;
// and you use it to splice the desired item off the array
state.meals.splice(inputIndex, 1);
...your other logic if any
},
You need to mutate your existing array
state.roundScore.splice(state.roundScore.findIndex((arrow) => arrow.id === action.payload), 1);
I'm trying to create a button to filter the data according to the entered value.
I have created a button in a header component and my project list in a main component.
My problem is that when I dispatch my 'projectFilter' action from my button in the header, it filters the data, but I lose the initial state.
There is my project slicer
import { createSlice } from "#reduxjs/toolkit";
let lastId = 2;
const slice = createSlice({
name: "projects",
initialState: [],
reducers: {
projectAdded: (state, action) => {
state.push({
id: ++lastId,
name: action.payload.name,
gender: action.payload.gender,
});
},
projectFilter: (state, action) => {
return state.filter((project) => project.id === action.payload.id);
},
},
});
export const { projectAdded, projectFilter } = slice.actions;
export default slice.reducer;
There is my two button for Adding a project and filtering a project
function Header() {
const dispatch = useDispatch();
const [value, setValue] = useState(0);
return (
<div className="header">
<button
onClick={() =>
dispatch(
projectAdded({ name: "project name", gender: "project gender" })
)
}
>
Add project
</button>
<input type="number" onChange={(event) => setValue(event.target.value)} />
<button onClick={() => dispatch(projectFilter({ id: Number(value) }))}>
Filter
</button>
</div>
);
}
export default Header;
And finally there is my list of project which I render in my page
function Main() {
const projects = useSelector((state) => state.projects);
return (
<div className="main">
{projects.map((project) => (
<p key={project.id}>
{project.id} {project.name} {project.gender}
</p>
))}
</div>
);
}
export default Main;
Could you please help me to filter data without loosing the all state ? Thank you in advance
Actions vs. Selectors
When you dispatch an action you are permanently changing the state. You do not want to replace state.projects with only the items matching the current filter. You want to keep all of your items in the state and select just the ones that match. This something that you do with the selector function inside of useSelector in Main.
Storing the Current Filter
Your selector function needs to know the filter id in order to filter matches. You can store that id in the component via useState or you can store the filter id as a property in your redux store (either in the projects slice or in another slice).
If you were editing the filter and displaying the filtered results from the same component, I would recommend the useState approach. With two separate components you can still do it, but the state would have to be stored in a shared parent and passed down via props. I don't know the relationship between your Main and Header components, so for this case I recommend storing the current filter in redux. You will dispatch an action to set the current filter. Basically that action just updates the stored filter id. The job of applying the filter still goes to to the selector.
lastId
Reducers should not rely on an external state. You don't want lastId to be just some variable in the file. You need to get an id from the state itself. You could look for the maximum id among the current projects, or you could store lastId as a property in the state.
Code
Slice
import { createSlice } from "#reduxjs/toolkit";
const initialState: {
projects: [],
lastId: 2, // why not 0 or 1?
filter: null, // is just the id right now, but could be a complex object
}
const slice = createSlice({
name: "projects",
initialState,
reducers: {
projectAdded: (state, action) => {
state.projects.push({
id: ++state.lastId,
name: action.payload.name,
gender: action.payload.gender,
});
},
projectFilter: (state, action) => {
state.filter = action.payload.id;
},
},
});
export const { projectAdded, projectFilter } = slice.actions;
export default slice.reducer;
No changes to Header
Main
function Main() {
const projects = useSelector(
// could move this function to another file
// this could be written more concisely, but I'm trying to be clear instead
(state) => {
const all = state.projects.projects;
const filterId = state.projects.filter;
if ( filterId === null ) {
return all;
}
else {
return all.filter( project => project.id === filterId );
}
});
return (
<div className="main">
{projects.map((project) => (
<p key={project.id}>
{project.id} {project.name} {project.gender}
</p>
))}
</div>
);
}
export default Main;
I am dynamically adding <div> elements to a component by adding them to an array. This is not a problem and works well. The issue I'm trying to solve here is removing the <div> on double click by passing the id of the <div> that was doubled clicked with props when the reducer is dispatched.
The main issue is the array filter function only works when I code hard the div id both on the div and in the filter function when I want to pass the id of e.target.id on dispatch of delDiv reducer.
Note: I can remove the div successfully by changing the addDivReducer like this:
case "ADD_DIV":
return state.concat(
<DivComponent
key={Math.floor(Math.random() * 100) + 1}
id={11} ***************************************************** Changed
/>
);
case "DELETE_DIV":
state = state.filter((elements) => {
return elements.props.id !== 11; *********************************** Changed
});
return state;
But the desired effect is to pass id as props on dispatch as seen in my code below
The reducer that adds a removes elements look like this:
import DivComponent from "../../components/AddDivComponent";
const addDivReducer = (state, action) => {
switch (action.type) {
case "ADD_DIV":
return state.concat(
<DivComponent
key={Math.floor(Math.random() * 100) + 1}
id={Math.floor(Math.random() * 100) + 1}
/>
);
case "DELETE_DIV":
state = state.filter((elements) => {
return elements.props.id !== action.payload;
});
return state;
default:
return (state = []);
}
};
export default addClipartReducer;
The actions index.js look like:
export const addDiv = (props) => {
return {
type: "ADD_DIV",
payload: props,
};
};
export const deleteDiv = (props) => {
return {
type: "DELETE_DIV",
payload: props,
};
};
The delete reducer is being dispatched when the div is double clicked on like this in AddDivComponent.js:
import { useDispatch } from "react-redux";
import { deleteDiv } from "../../store/actions";
const AddDivComponent = (props) => {
const dispatch = useDispatch();
const removeClipart = (e) => {
dispatch(deleteDiv(e.target.id));
};
return(
<div
id={props.id}
className="my-div"
onDoubleClick={removeDiv}
/>
);
};
export default DivComponent;
Finally the array of <div> elements is being shown here in Canvas.js:
import { useSelector } from "react-redux";
const Canvas = () => {
const divList = useSelector((state) => state.addDIV);
return(
<div className="canvas">
{divList}
</div>
);
};
export default Canvas;
you are mutating state at your DELETE_DIV reducer. If you need to handle state, create a copy a first:
// mutating state here to a new value, can lead to problems
state = state.filter((elements) => {
return elements.props.id !== action.payload;
});
I would suggest to return filter directly, given filter already returns the desired next state, while not mutating the original:
case "DELETE_DIV":
return state.filter((elements) => {
return elements.props.id !== action.payload;
});
Im struggling to achieve infinite scroll with my test React/Redux application.
Here how it works in simple words:
1) On componentDidMount I dispatch an action which sets the Redux state after getting 100 photos from the API. So I got photos array in Redux state.
2) I implemented react-waypoint, so when you scroll to the bottom of those photos it fires a method which dispatches another action that get more photos and "appends" them to the photos array and...
as I understand - the state changed, so redux is firing the setState and the component redraws completely, so I need to start scrolling again but its 200 photos now. When I reach waypoint again everything happens again, component fully rerenders and I need to scroll from top through 300 photos now.
This is not how I wanted it to work of course.
The simple example on react-waypoint without Redux works like this:
1) you fetch first photos and set the components initial state
2) after you scroll to the waypoint it fires a method which makes another request to the api, constructs new photos array(appending newly fetched photos) and (!) call setState with the new photos array.
And it works. No full re-renders of the component. Scroll position stays the same, and the new items appear below waypoint.
So the question is — is the problem I experience the problem with Redux state management or am I implementing my redux reducers/actions not correctly or...???
Why is setting component state in React Waypoint Infinite Scroll example (no Redux) works the way I want (no redrawing the whole component)?
I appreciate any help! Thank you!
The reducers
import { combineReducers } from 'redux';
const data = (state = {}, action) => {
if (action.type === 'PHOTOS_FETCH_DATA_SUCCESS') {
const photos = state.photos ?
[...state.photos, ...action.data.photo] :
action.data.photo;
return {
photos,
numPages: action.data.pages,
loadedAt: (new Date()).toISOString(),
};
}
return state;
};
const photosHasErrored = (state = false, action) => {
switch (action.type) {
case 'PHOTOS_HAS_ERRORED':
return action.hasErrored;
default:
return state;
}
};
const photosIsLoading = (state = false, action) => {
switch (action.type) {
case 'PHOTOS_IS_LOADING':
return action.isLoading;
default:
return state;
}
};
const queryOptionsIntitial = {
taste: 0,
page: 1,
sortBy: 'interestingness-asc',
};
const queryOptions = (state = queryOptionsIntitial, action) => {
switch (action.type) {
case 'SET_TASTE':
return Object.assign({}, state, {
taste: action.taste,
});
case 'SET_SORTBY':
return Object.assign({}, state, {
sortBy: action.sortBy,
});
case 'SET_QUERY_OPTIONS':
return Object.assign({}, state, {
taste: action.taste,
page: action.page,
sortBy: action.sortBy,
});
default:
return state;
}
};
const reducers = combineReducers({
data,
photosHasErrored,
photosIsLoading,
queryOptions,
});
export default reducers;
Action creators
import tastes from '../tastes';
// Action creators
export const photosHasErrored = bool => ({
type: 'PHOTOS_HAS_ERRORED',
hasErrored: bool,
});
export const photosIsLoading = bool => ({
type: 'PHOTOS_IS_LOADING',
isLoading: bool,
});
export const photosFetchDataSuccess = data => ({
type: 'PHOTOS_FETCH_DATA_SUCCESS',
data,
});
export const setQueryOptions = (taste = 0, page, sortBy = 'interestingness-asc') => ({
type: 'SET_QUERY_OPTIONS',
taste,
page,
sortBy,
});
export const photosFetchData = (taste = 0, page = 1, sort = 'interestingness-asc', num = 500) => (dispatch) => {
dispatch(photosIsLoading(true));
dispatch(setQueryOptions(taste, page, sort));
const apiKey = '091af22a3063bac9bfd2e61147692ecd';
const url = `https://api.flickr.com/services/rest/?api_key=${apiKey}&method=flickr.photos.search&format=json&nojsoncallback=1&safe_search=1&content_type=1&per_page=${num}&page=${page}&sort=${sort}&text=${tastes[taste].keywords}`;
// console.log(url);
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
dispatch(photosIsLoading(false));
return response;
})
.then(response => response.json())
.then((data) => {
// console.log('vvvvv', data.photos);
dispatch(photosFetchDataSuccess(data.photos));
})
.catch(() => dispatch(photosHasErrored(true)));
};
I also include my main component that renders the photos because I think maybe it's somehow connected with the fact that i "connect" this component to Redux store...
import React from 'react';
import injectSheet from 'react-jss';
import { connect } from 'react-redux';
import Waypoint from 'react-waypoint';
import Photo from '../Photo';
import { photosFetchData } from '../../actions';
import styles from './styles';
class Page extends React.Component {
loadMore = () => {
const { options, fetchData } = this.props;
fetchData(options.taste, options.page + 1, options.sortBy);
}
render() {
const { classes, isLoading, isErrored, data } = this.props;
const taste = 0;
const uniqueUsers = [];
const photos = [];
if (data.photos && data.photos.length > 0) {
data.photos.forEach((photo) => {
if (uniqueUsers.indexOf(photo.owner) === -1) {
uniqueUsers.push(photo.owner);
photos.push(photo);
}
});
}
return (
<div className={classes.wrap}>
<main className={classes.page}>
{!isLoading && !isErrored && photos.length > 0 &&
photos.map(photo =>
(<Photo
key={photo.id}
taste={taste}
id={photo.id}
farm={photo.farm}
secret={photo.secret}
server={photo.server}
owner={photo.owner}
/>))
}
</main>
{!isLoading && !isErrored && photos.length > 0 && <div className={classes.wp}><Waypoint onEnter={() => this.loadMore()} /></div>}
{!isLoading && !isErrored && photos.length > 0 && <div className={classes.wp}>Loading...</div>}
</div>
);
}
}
const mapStateToProps = state => ({
data: state.data,
options: state.queryOptions,
hasErrored: state.photosHasErrored,
isLoading: state.photosIsLoading,
});
const mapDispatchToProps = dispatch => ({
fetchData: (taste, page, sort) => dispatch(photosFetchData(taste, page, sort)),
});
const withStore = connect(mapStateToProps, mapDispatchToProps)(Page);
export default injectSheet(styles)(withStore);
Answer to Eric Na
state.photos is an object and I just check if its present in the state. sorry, in my example I just tried to simplify things.
action.data.photo is an array for sure. Api names it so and I didn't think about renaming it.
I supplied some pics from react dev tools.
Here is my initial state after getting photos
Here is the changed state after getting new portion of photos
There were 496 photos in the initial state, and 996 after getting
additional photos for the first time after reaching waypoint
here is action.data
So all I want to say that the photos are fetched and appended but it triggers whole re-render of the component still...
I think I see the problem.
In your component you check for
{!isLoading && !isErrored && photos.length > 0 &&
photos.map(photo =>
(<Photo
key={photo.id}
taste={taste}
id={photo.id}
farm={photo.farm}
secret={photo.secret}
server={photo.server}
owner={photo.owner}
/>))
}
once you make another api request, in your action creator you set isLoading to true. this tells react to remove the whole photos component and then once it's set to false again react will show the new photos.
you need to add a loader at the bottom and not to remove the whole photos component once fetching and then render it again.
EDIT2
Try commenting out the whole uniqueUsers part (let's worry about the uniqueness of the users later)
const photos = [];
if (data.photos && data.photos.length > 0) {
data.photos.forEach((photo) => {
if (uniqueUsers.indexOf(photo.owner) === -1) {
uniqueUsers.push(photo.owner);
photos.push(photo);
}
});
}
and instead of
photos.map(photo =>
(<Photo ..
try directly mapping data.photos?
data.photos.map(photo =>
(<Photo ..
EDIT
...action.data.photo] :
action.data.photo;
can you make sure it's action.data.photo, not action.data.photos, or even just action.data? Can you try logging the data to the console?
Also,
state.photos ? .. : ..
Here, state.photos will always evaluate to true-y value, even if it's an empty array. You can change it to
state.photos.length ? .. : ..
It's hard to tell without actually seeing how you update photos in reducers and actions, but I doubt that it's the problem with how Redux manages state.
When you get new photos from ajax request, the new photos coming in should be appended to the end of the photos array in the store.
For example, if currently photos: [<Photo Z>, <Photo F>, ...] in Redux store, and the new photos in action is photos: [<Photo D>, <Photo Q>, ...], the photos in store should be updated like this:
export default function myReducer(state = initialState, action) {
switch (action.type) {
case types.RECEIVE_PHOTOS:
return {
...state,
photos: [
...state.photos,
...action.photos,
],
};
...
Situation
I have a reactjs app using redux. I've got a container, and a presentation component. My presentation component renders a form with an array of simple text input fields, and I'm passing an 'onChange' callback function from the container to the presentation component which dispatches an action to change redux state when one of the text fields is typed in.
Problem
The presentation component successfully renders the correct values for the text fields from redux state when the presentation component mounts, but doesn't update when I type in the fields. If I log the props that are passed to the presentation component in mapStateToProps in the container, I can see that the onChange function is correctly dispatching to the store, and the redux state is being updated correctly. But the presentation component is not re-rendering when this happens, so typing in the text field doesn't update the view (typing does nothing).
formConnector
import { connect } from 'react-redux'
import Form from '../components/Form'
import { changeElementValue } from '../actions/actions'
const mapStateToProps = (state) => {
//e.g. state.elements = [{id:"email", value:"foo#bar.com"}]
let props = {
elements: state.elements,
}
//state and props.elements.{id}.value changes successfully when I
//type in one of the input fields, but
//the the Form component is not re-rendered
console.log(props)
return props
}
const mapDispatchToProps = (dispatch) => {
return {
onElementChange: (id, value) => {
dispatch(changeElementValue(id, value))
},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Form)
Form reducer
function formReducer(state = initialState, action = null) {
switch(action.type) {
case types.CHANGE_ELEMENT_VALUE:
let newState = Object.assign({}, state)
newState.elements[action.id].value = action.value
return newState
default:
return state;
}
}
actions
import * as types from './actionTypes'
export function changeElementValue(id, value) {
return { type: types.CHANGE_ELEMENT_VALUE, id, value }
}
As discussed in the comments, this is due to state mutation.
Try to change your reducer code as follows:
case types.CHANGE_ELEMENT_VALUE: {
const newElements = state.elements.map(function(el, index) {
return action.id === index
? Object.assign({}, el, { value: action.value })
: el;
});
return Object.assign({}, state, { elements: newElements );
}
or more elegantly:
case types.CHANGE_ELEMENT_VALUE:
return { ...state, elements: state.elements.map((el, index) => (
action.id === index ? { ...el, value: action.value } : el
)}