I have an issue where re-rendering of state causes ui issues and was suggested to only update specific value inside my reducer to reduce amount of re-rendering on a page.
this is example of my state
{
name: "some name",
subtitle: "some subtitle",
contents: [
{title: "some title", text: "some text"},
{title: "some other title", text: "some other text"}
]
}
and I am currently updating it like this
case 'SOME_ACTION':
return { ...state, contents: action.payload }
where action.payload is a whole array containing new values. But now I actually just need to update text of second item in contents array, and something like this doesn't work
case 'SOME_ACTION':
return { ...state, contents[1].text: action.payload }
where action.payload is now a text I need for update.
You can use map. Here is an example implementation:
case 'SOME_ACTION':
return {
...state,
contents: state.contents.map(
(content, i) => i === 1 ? {...content, text: action.payload}
: content
)
}
You could use the React Immutability helpers
import update from 'react-addons-update';
// ...
case 'SOME_ACTION':
return update(state, {
contents: {
1: {
text: {$set: action.payload}
}
}
});
Although I would imagine you'd probably be doing something more like this?
case 'SOME_ACTION':
return update(state, {
contents: {
[action.id]: {
text: {$set: action.payload}
}
}
});
Very late to the party but here is a generic solution that works with every index value.
You create and spread a new array from the old array up to the index you want to change.
Add the data you want.
Create and spread a new array from the index you wanted to change to the end of the array
let index=1;// probably action.payload.id
case 'SOME_ACTION':
return {
...state,
contents: [
...state.contents.slice(0,index),
{title: "some other title", text: "some other text"},
...state.contents.slice(index+1)
]
}
Update:
I have made a small module to simplify the code, so you just need to call a function:
case 'SOME_ACTION':
return {
...state,
contents: insertIntoArray(state.contents,index, {title: "some title", text: "some text"})
}
For more examples, take a look at the repository
function signature:
insertIntoArray(originalArray,insertionIndex,newData)
Edit:
There is also Immer.js library which works with all kinds of values, and they can also be deeply nested.
You don't have to do everything in one line:
case 'SOME_ACTION': {
const newState = { ...state };
newState.contents =
[
newState.contents[0],
{title: newState.contents[1].title, text: action.payload}
];
return newState
};
I believe when you need this kinds of operations on your Redux state the spread operator is your friend and this principal applies for all children.
Let's pretend this is your state:
const state = {
houses: {
gryffindor: {
points: 15
},
ravenclaw: {
points: 18
},
hufflepuff: {
points: 7
},
slytherin: {
points: 5
}
}
}
And you want to add 3 points to Ravenclaw
const key = "ravenclaw";
return {
...state, // copy state
houses: {
...state.houses, // copy houses
[key]: { // update one specific house (using Computed Property syntax)
...state.houses[key], // copy that specific house's properties
points: state.houses[key].points + 3 // update its `points` property
}
}
}
By using the spread operator you can update only the new state leaving everything else intact.
Example taken from this amazing article, you can find almost every possible option with great examples.
This is remarkably easy in redux-toolkit, it uses Immer to help you write immutable code that looks like mutable which is more concise and easier to read.
// it looks like the state is mutated, but under the hood Immer keeps track of
// every changes and create a new state for you
state.x = newValue;
So instead of having to use spread operator in normal redux reducer
return {
...state,
contents: state.contents.map(
(content, i) => i === 1 ? {...content, text: action.payload}
: content
)
}
You can simply reassign the local value and let Immer handle the rest for you:
state.contents[1].text = action.payload;
Live Demo
In my case I did something like this, based on Luis's answer:
// ...State object...
userInfo = {
name: '...',
...
}
// ...Reducer's code...
case CHANGED_INFO:
return {
...state,
userInfo: {
...state.userInfo,
// I'm sending the arguments like this: changeInfo({ id: e.target.id, value: e.target.value }) and use them as below in reducer!
[action.data.id]: action.data.value,
},
};
Immer.js (an amazing react/rn/redux friendly package) solves this very efficiently. A redux store is made up of immutable data - immer allows you to update the stored data cleanly coding as though the data were not immutable.
Here is the example from their documentation for redux:
(Notice the produce() wrapped around the method. That's really the only change in your reducer setup.)
import produce from "immer"
// Reducer with initial state
const INITIAL_STATE = [
/* bunch of todos */
]
const todosReducer = produce((draft, action) => {
switch (action.type) {
case "toggle":
const todo = draft.find(todo => todo.id === action.id)
todo.done = !todo.done
break
case "add":
draft.push({
id: action.id,
title: "A new todo",
done: false
})
break
default:
break
}
})
(Someone else mentioned immer as a side effect of redux-toolkit, but you should use immer directly in your reducer.)
Immer installation:
https://immerjs.github.io/immer/installation
This is how I did it for one of my projects:
const markdownSaveActionCreator = (newMarkdownLocation, newMarkdownToSave) => ({
type: MARKDOWN_SAVE,
saveLocation: newMarkdownLocation,
savedMarkdownInLocation: newMarkdownToSave
});
const markdownSaveReducer = (state = MARKDOWN_SAVED_ARRAY_DEFAULT, action) => {
let objTemp = {
saveLocation: action.saveLocation,
savedMarkdownInLocation: action.savedMarkdownInLocation
};
switch(action.type) {
case MARKDOWN_SAVE:
return(
state.map(i => {
if (i.saveLocation === objTemp.saveLocation) {
return Object.assign({}, i, objTemp);
}
return i;
})
);
default:
return state;
}
};
I'm afraid that using map() method of an array may be expensive since entire array is to be iterated. Instead, I combine a new array that consists of three parts:
head - items before the modified item
the modified item
tail - items after the modified item
Here the example I've used in my code (NgRx, yet the machanism is the same for other Redux implementations):
// toggle done property: true to false, or false to true
function (state, action) {
const todos = state.todos;
const todoIdx = todos.findIndex(t => t.id === action.id);
const todoObj = todos[todoIdx];
const newTodoObj = { ...todoObj, done: !todoObj.done };
const head = todos.slice(0, todoIdx - 1);
const tail = todos.slice(todoIdx + 1);
const newTodos = [...head, newTodoObj, ...tail];
}
Pay attention to the data structure:
in a project I have data like this
state:{comments:{items:[{...},{...},{...},...]} and to update one item in items I do this
case actionTypes.UPDATE_COMMENT:
const indexComment = state.comments.items.findIndex(
(comment) => comment.id === action.payload.data.id,
);
return {
...state,
comments: {
...state.comments,
items: state.comments.items.map((el, index) =>
index === indexComment ? { ...el, ...action.payload.data } : el,
),
},
};
Note: in newer versions (#reduxjs/toolkit), Redux automatically detects changes in object, and you don't need to return a complete state :
/* reducer */
const slice = createSlice({
name: 'yourweirdobject',
initialState: { ... },
reducers: {
updateText(state, action) {
// updating one property will cause Redux to update views
// only depending on that property.
state.contents[action.payload.id].text = action.payload.text
},
...
}
})
/* store */
export const store = configureStore({
reducer: {
yourweirdobject: slice.reducer
}
})
This is how you should do now.
Related
I have a problem with my code. I currently have some data like the one below;
users: [
{
name: 'bolu',
features: ['Tall'],
},
{
name: 'cam',
features: ['Bearded', 'Short'],
},
],
};
What I am trying to do is delete/remove a single feature - for example if I pass in 'short' into my redux action. I'd like for it (the 'Short' text) to be removed from the features array. I currently have my redux action set up this way:
export interface UsersDataState {
name: string,
features: Array<string>,
}
export interface UsersState {
users: UsersDataState[];
}
const initialState: UsersState = {
users: [],
};
export const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
removeUser: (state, action: PayloadAction<string>) => {
const removedUsers = state.users.filter((user) => user.features.indexOf(action.payload));
state.users = removedUsers;
},
},
});
So here I am passing in the value in (action.payload is the value being passed in). When this action is dispatched, I want to remove just the word that is passed in from the features array. I hope this is clearer now.
This doesn't work for some reason and I am unable to figure out why. Any help would be appreciated please, thank you.
Your code doesn't match your state structure. Replace traits with users, and values with features.
It looks like that's a part of a reducer, not an action (which is an object, not a function).
You should be returning a new state from the reducer.
Given your update the function should be called removeFeature.
So, I've corrected a few bits of your code based on what I remember from Redux. Note: contrived example.
// State
const state={users:[{name:"joe",features:["Mean","Short"]},{name:"bolu",features:["Tall"]},{name:"cam",features:["Bearded","Short"]}]};
// The reducer accepts a state, and an action
function reducer(state, action) {
// We destructure the type, and payload, from the action object
const { type, payload } = action;
// Depending on the type...
switch (type) {
case 'removeFeature': {
// `map` over the users (we return a new state array)
return state.users.map(user => {
// `filter` out the feature elements
// that match the payload
const updatedFeatures = user.features.filter(feature => {
return feature !== payload;
});
// Return a new updated object
return { ...user, features: updatedFeatures };
});
}
default: return state;
}
}
const updatedState = reducer(state, {
type: 'removeFeature',
payload: 'Short'
});
console.log(updatedState);
Learning React with Redux, the course author wrote this:
export default (state = [], action) => {
switch (action.type) {
case FETCH_STREAM:
return { ...stream, [action.payload.id]: action.payload };
case CREATE_STREAM:
return { ...stream, [action.payload.id]: action.payload };
case EDIT_STREAM:
return { ...stream, [action.payload.id]: action.payload };
default:
return state;
}
};
As much as I understood, the { ...stream, [action.payload.id]: action.payload } pattern is a new JavaScript syntax called "Key Interpolation Syntax" and what it does is that it adds a new key/value to our object ( which we have made a copy of it with spread ... syntax first ) so in that case I do understand the CREATE_STREAM one, but how about EDIT_STREAM ? Is it gonna replace the key because the key is already in there? or is it gonna blindly add it so then duplicate key? Also I don't understand the FETCH_STREAM at all. Why is it even like this? Shouldn't it just be a return action.payload ?
Actually, the Key Interpolation syntax will add that key if it doesn't exist but will override its value if it is duplicated.
The following code
const personA = {
name: 'John Doe',
age: 42
};
const createdProp = 'country';
const overriddenProp = 'age';
const personB = {
...personA,
[createdProp]: 'USA',
[overriddenProp]: 50
};
Will result in personB with these values
{
name: "John Doe",
age: 50,
country: "USA"
}
About the FETCH it depends on what they intended. If the spread operation is removed, then all previous data will be lost. If that is no problem, you can do { [action.payload.id]: action.payload } without worries.
I have this store:
const initialActors = {
list: 'Actor\'s lists',
actors: [
{
name: 'Angelina Jole',
involved: true
},
{
name: 'Bratt Pitt',
involved: false
},
]
}
I have a reducer to add a new actor to my store:
const actors = (state = initialActors, action) => {
switch(action.type){
case 'ADD_NEW':
return {
...state.list,
actors: [
...state.actors,
action.name,
action.involved
]
}
default:
return state
}
}
I have a action creator too but it doesn't important. I dispatch that and display my state:
store.dispatch(addNew({name: "Hello", involved: true}) ); // I have this action creator
console.log(store.getState()
console.log displays very difficult object with letters like: {0: "A", 1: "c"}. What is wrong?
#Edit
I tryed change ...state.list to state list like this:
const actors = (state = initialActors, action) => {
switch(action.type){
case 'ADD_NEW':
return {
state.list,
actors: [
...state.actors,
action.name,
action.involved
]
}
default:
return state
}
}
but I have this error:
Parsing error: Unexpected token, expected ","
Similiar situation I have if I tryed modify actors array:
case 'ADD_NEW':
return {
...state.list,
actors: [
...state.actors,
{
action.name,
action.involved
}
]
}
Error message is this same but point at action object (action.name).
In your reducer, you are mistakenly spreading state.list. Since it is a string, you are getting all the letters. You should spread the state there. Because, you want to keep your state, other than actors array. This is why you spread the whole state and keep the list (with others if there would be).
Also, you are adding your item wrong, it should be an object.
const actors = (state = initialActors, action) => {
const { name, involved } = action;
switch (action.type) {
case "ADD_NEW":
return {
...state,
actors: [
...state.actors,
{
name,
involved
}
]
};
default:
return state;
}
};
state.list is a string, and you are trying to spread it
let a = 'name'
let c = {...a}
console.log(c)
run the above code snippet so you will be able to understand it
and for adding new object you need to update the reducer as follows
...state,
actors: [
...state.actors,
{
name: action.name,
involved: action.involved
}
]
so the above code will spread all the existing properties in the state and then you spread all the current actors and add a new object as shown above
Update
Since you are passing as
store.dispatch(addNew({name: "Hello", involved: true}) );
here the payload is an object so you can directly use that one
so in action it will be object which has two props one is
type
the payload you have send it
...state,
actors: [
...state.actors,
action.payload
]
Sample Working codesandbox
Just a small mistake what you did, you need to add as an object with 2 properties like [...state.actors, { action.name, action.involved }] instead of what you did.
From Spread syntax's documentation:
Spread syntax allows an iterable such as an array expression or string to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected, or an object expression to be expanded in places where zero or more key-value pairs (for object literals) are expected.
Like this:
const actors = (state = initialActors, action) => {
switch(action.type){
case 'ADD_NEW':
return {
...state,
actors: [
...state.actors,
{
name: action.name,
involved: action.involved
}
]
}
default:
return state
}
}
Consider the following:
var array = [
{
name: 'someone',
involved: false,
}
];
console.log([...array, { name: 'hello', involved: true }]);
I hope this helps!
I've met some trouble assigning a new object in the reducer of my app. My state contains 2 arrays :
{
elements: [],
constraints: []
}
Those elements are handled by 2 reducers :
elementsReducer
constraintsReducer
and combined like this:
let reducer = combineReducers({
elements: elementsReducer,
constraints: constraintsReducer
});
export default reducer
So, basically, an action is triggered, and my reducer is supposed to update all the state.elements array. I've tried several things and I can't update the whole elements array, only - in the best case - the first element.
My first idea was to do:
return Object.assign({}, state, {
elements: state.map((e) => {
return Object.assign({}, e, {
text: action.data[e.id][e.text]
})
})
});
action.data is an array containing a different text for each element. Basically, all I was to do is, on a special action, updating all the element array. But this syntax does not work as it creates a new array INSIDE the array "elements" of the store. It does not replace it. If I let this, the store becomes:
{
elements: [
elements: [...]
],
constraints: [...]
}
When I access the state in my reducer elementsReducer, it's only the "element" array and not the full state. After this issue, I've tried to do the following:
return state.map(function(e) {
return assign({}, e, {
text: action.data[e.id][e.text]
});
});
Now, I worked, but the ONLY element mapped is the first one. The other elements are simply not updating.
Do you have any idea to solve the issue?
Thanks everyone :)
Xelys
EDIT :
// code of elementsReducer
var assign = require('object-assign');
export default function elementsReducer(state = {}, action) {
switch (action.type) {
case 'ADD_ELEMENT':
return [...state,
{
name: action.name,
id: action.id,
committed: false,
text: action.text
}
]
case 'COMMIT_ELEMENT':
console.log('commit action')
return state.map(function(e) {
return e.id === action.id ?
assign({}, e, {committed: true}) :
e
});
case 'SAVE_DATA':
return state.map((e) => {
return Object.assign({}, e, {
text: action.data[e.id][e.text]
});
});
default:
return state;
}
}
Based on your code, I assumed your data structure is like below:
// state.element
stateElement = [
{ id:1, text: '1t' },
{ id:2, text: '2t' }
];
// Your action result
action = {
data: {
1: { text: 'new 1t' },
2: { text: 'new 2t' }
}
}
// Your new state.element
newData = data.map(function(e) {
return Object.assign({}, e, {
text: action.data[e.id].text
});
});
thanks for the answers.
#Ali Sepehri.Kh, yeah, my data structure is very similar. Actually, it's a little bit more complex, but I've simplified it to be more understable.
However, I figured out to solve the issue. I feel quite ashamed of creating a post on stackoverflow, because the error had nothing to do with redux.
The mapping function I've used was totally working. The issue was located is the action data. The function which created the action.data array was returning an array empty after the first element. I thought the issue was coming from the map() as it was for me the "most difficult" part of the fonction.
However, I've made a stupid mistake on the fonction creating the array of the action. I have misplaced a "return" inside a for loop, which explain that all the elements after the first one were empty.
Sorry for your time guys, and thanks for the help :).
I´ve written a small toDo-list app with Database interaction. Everything works fine, except that when I try to remove a toDo-list item from my state, the object-state structure is changed.
Therefore I´m not longer able to render my ToDo-list on runtime.
I assume that just a small change in my reducer code is required, but I´m a little bit to experienced with ES6 & Redux to get it on my own.
This is my Reducer Code:
export default (state = {items: []}, action) => {
switch (action.type){
case 'RECEIVE_POSTS':
return Object.assign({}, state, {
items: action.items
});
case 'ADD_ITEM':
return Object.assign({}, state, {
items: [
...state.items,
action.items
]
});
case 'REMOVE_ITEM':
return Object.assign({}, state, {
items: [
state.items.filter((items, id) => id !== action.id)
]
});
default:
return state
}
}
And my deleteItem - Action
export default function deleteItem(id){
console.log("test")
return (dispatch, getState) => {
return dispatch (removeItem(dispatch, id))
return dispatch (receiveDelete(id))
}
}
function removeItem(dispatch, id){
return dispatch => {
$.ajax({
method: "POST",
url: "http://localhost:444/localWebServices/removeTask.php",
data: {id: id}
}).success(function(msg){
console.log(msg)
dispatch(receiveDelete(id))
});
}
}
function receiveDelete(id){
console.log("receive add")
return {
type: 'REMOVE_ITEM',
id: id
}
}
And Here a two Images of the Object Structure before and after I deleted an Item.
To make this clear. Everything works (the list is rendered), but after I deleted an Item the State structure is changed -> List not rendered. I don´t get any Error msg.
Before delete:
After delete:
Any help would be great!
Update:
I have changed my Code and do not return an array anymore. According to the dev console the state seems to be correct now, but it´s not rendering. Does this code goes in the right direction?
return Object.assign({}, state, {
items: state.items.filter((items, id) => id !== action.id)
});
Arguments passed to filter callback are arrayElement, index, array so try changing your callback to .filter(item => item.id !== action.id)