Access the state in an action in redux - javascript

Many links and tutorials advice to group the logic in action creators in order to simplify the reducer logic.
Imagine a simple (normalized) state:
const initialState = {
parent: {
allIds: [0],
byId: {
0: {
parentProperty: `I'm the parent`,
children: [1, 2]
}
}
},
children: {
allIds: [1, 2],
byId: {
1: {
childrenProperty: `I'm the children`
},
2: {
childrenProperty: `I'm the children`
}
}
}
}
I would now delete the parent. Since only the parent refers to the children, I would also delete the children too.
I imagine a action creator like this:
function deleteParents(parentId) {
return {type: 'DELETE_PARENT', payload: {parentId}};
}
and
function deleteChild(childId) {
return {type: 'DELETE_CHILD', payload: {childId}};
}
For now, to manage this case I do something like this (with redux-thunk)
function deleteParentAndChildren(parentId) {
return (dispatch, getState) {
const childrenIds = myChildrenSelector(getState(), parentId);
const deleteChildrenActions = childrenIds.map(deleteChild);
const deleteParentAndChildrenAction = batchActions([
deleteParents(parentId),
...deleteChildrenActions
], 'DELETE_PARENT_AND_CHILDREN');
dispatch(deleteParentAndChildrenAction);
}
}
In that way, I compose little action into big one, and the reducer logic is very simple because it only consist to delete a key in an object.
Conversely, I don't like to use redux-thunk (used to async actions) just to get the state (and this is considered as anti pattern).
How do you guys manage this type of problems ?
Does a tool like redux-sage may help ?

The problematic you seem to have seems fairly common and if your application is a bit sophisticated I would suggest using redux-orm which is a bit hard to understand and then integrate but once set up it just abstract you all the hard relationship work between your entities.

I opine differently here. The way I would do it is, I would delete child while deleting parent but not y dispatching child_delete action. When you create new state while deleting parent in reducer, at that time, you can access the children referred by that parent, and remove those as well from state.
Also, if you still want to dispatch actions separately, you can pass childids from component to action. From that action, you can dispatch two different actions, deleting parent and chilren ids.
----------- EDIT -------
// The initial application state
const initialState = {
parent: {
allIds: [0],
byId: {
0: {
parentProperty: `I'm the parent`,
children: [1, 2]
}
}
},
children: {
allIds: [1, 2],
byId: {
1: {
childrenProperty: `I'm the children`
},
2: {
childrenProperty: `I'm the children`
}
}
}
}
export default function batchManagement(state = initialState, action) {
switch (action.type) {
case 'DELETE_PARENT': //assuming action.deleteParents = [0]
//Here you have access to state, so you can change parents and children as well, and create a totally new state
case 'DELETE_CHILDREN':
//Return a new object for state
default:
return state;
}
}

Related

how to change initialState in react-redux

I'm using react-redux to fetch data from MongoDB database and put it into React App.
I've following structure to work upon:
const initialState = {
Level: [{
wId: Math.random(),
Level1: [
{
id: Math.random(),
item1: 'item1',
item2: 'item2'
},
.......
],
Level2: [
{
id: Math.random(),
item1: 'item1',
item2: 'item2'
},
.......
]
}]
}
Redux Function:
export default function (state = initialState, action) {
switch (action.type) {
case GET_ITEMS:
return {
...state,
// what should i write here to get above mentioned state structure
}
..............
}
Note:
Initially Level is empty. So if new data is received in payload then the following structure should be formed.
How to update particular item like item1 at level2
Sample Input:
action.payload = {
id1: 23234, // should be assigned in Wid
ITEM: [ // Level1
{
id2: 89724, // should be assigned in Level1.id
i: 'abc', // should be assigned in Level1.item1
j: 'xyz' // should be assigned in Level1.item2
}
]
}
I you dont know how many items you are going to get its would be difficult. One way to work around this issue could compare the previos state with current state and update only necessary part that got changed.
You can use number of libraries or follow any answer in How to determine equality for two JavaScript objects? to compare the objects.
Ideally you would need different actions to update Level, Level ->Level 1 and so on.
Create separate actions for adding levels. Call that action when on user events which add a level to your initial state.
export default function (state = initialState, action) {
switch (action.type) {
case GET_ITEMS:
return {
...state,
// what should i write here to get above mentioned state structure
}
case ADD_LEVELS:
return {
...state,
Level: [...state.Level, action.payload.Level]
}
}
You can move the id generation logic to the component as it will make your life simpler.

"Updating" all state in redux app properly

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 :).

How to update single value inside specific array item in redux

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.

Redux see other state from Reducer

Imagine a UI with two React components:
<FilterContainer />
<UserListContainer />
We pull down an array of users from the server:
[
{
name: 'John',
enjoys: ['Sailing', 'Running']
},
{
name: 'Bob',
enjoys: ['Running', 'Eating']
},
{
name: 'Frank',
enjoys: ['Sailing', 'Eating']
}
]
The UI looks a little like this:
Filter: Sailing Running Eating
UserList:
John
Frank
You can click on either a filter or a user. To get to this stage, we've clicked on 'Sailing' and then on 'Frank' (maybe we see a nice photo of Frank in the middle of the screen).
My Redux state, built using combineReducers, looks like this:
{
ui: {
filter: {enjoys: 'Sailing'},
userList: {selected: 'John'}
}
data: [user array]
}
I have two actions, SELECT_USER and SELECT_FILTER.
When I click on a filter (SELECT_FILTER fires), I want the ui.userList.selected to persist if that user is still in the filter, and the ui.userList.selected to be set to null if the user is not in the filter.
So if I now click on Eating, I'll see a list with Bob and Frank in it, and Frank is selected. But if I click on Running, I'll see John and Bob, but neither are selected.
However I'm struggling to do this in the conventional Redux methodology. When the userList reducer sees the SELECT_FILTER action, there's no way for it to check the data state to see if the currently selected user is still in that filter condition or not.
What's the right way to do this?
function filter(state = {enjoys: null}, action) {
switch (action.type) {
case SELECT_FILTER:
return {
...state,
enjoys: action.enjoys
}
default:
return state
}
}
function userList(state = {selected: null}, action) {
switch (action.type) {
case SELECT_USER:
return {
...state,
selected: action.name
}
default:
return state
}
}
const ui = combineReducers({
filter,
userList
})
let initialUsers = [
{
name: 'John',
enjoys: ['Sailing', 'Running']
},
{
name: 'Bob',
enjoys: ['Running', 'Eating']
},
{
name: 'Frank',
enjoys: ['Sailing', 'Eating']
}
]
const rootReducer = combineReducers({
ui,
data: (state=initialUsers) => state // in reality, loaded from server
})
export default rootReducer
Reducer should be aware only of a small part of state.
Good place for described logic is the action creator. With redux-thunk you will be able to make a decision based on a global state.
function selectFilter(enjoys) {
return (dispatch, getState) => {
dispatch({type: SELECT_FILTER, enjoys});
// if getState().ui.userList.selected exists in getState().data
dispatch({type: SELECT_USER, name: null});
}
};
You need another action for this.
If you are filtering the users in the data reducer, you will need to dispatch an action in one of your components' hooks (componentWillUpdate or componentWillReceiveProps) when you detect that the array of users has changed. This action will provide your filter reducer with the current array of users, and there you can set the selected field as you like.
If you are filtering the users in the server, I guess you already have an action like FETCH_USERS_SUCCESS that you can use for this.
It should be handled by the filter reducer. You need to send the users data as part of the action payload. Hence the reducer could Calc the selected user logic. You should consider adding a new action, as #cuttals suggested.

Updating nested data in redux store

What's the best/correct way to update a nested array of data in a store using redux?
My store looks like this:
{
items:{
1: {
id: 1,
key: "value",
links: [
{
id: 10001
data: "some more stuff"
},
...
]
},
...
}
}
I have a pair of asynchronous actions that updates the complete items object but I have another pair of actions that I want to update a specific links array.
My reducer currently looks like this but I'm not sure if this is the correct approach:
switch (action.type) {
case RESOURCE_TYPE_LINK_ADD_SUCCESS:
// TODO: check whether the following is acceptable or should we create a new one?
state.items[action.resourceTypeId].isSourceOf.push(action.resourceTypeLink);
return Object.assign({}, state, {
items: state.items,
});
}
Jonny's answer is correct (never mutate the state given to you!) but I wanted to add another point to it. If all your objects have IDs, it's generally a bad idea to keep the state shape nested.
This:
{
items: {
1: {
id: 1,
links: [{
id: 10001
}]
}
}
}
is a shape that is hard to update.
It doesn't have to be this way! You can instead store it like this:
{
items: {
1: {
id: 1,
links: [10001]
}
},
links: {
10001: {
id: 10001
}
}
}
This is much easier for update because there is just one canonical copy of any entity. If you need to let user “edit a link”, there is just one place where it needs to be updated—and it's completely independent of items or anything other referring to links.
To get your API responses into such a shape, you can use normalizr. Once your entities inside the server actions are normalized, you can write a simple reducer that merges them into the current state:
import merge from 'lodash/object/merge';
function entities(state = { items: {}, links: {} }, action) {
if (action.response && action.response.entities) {
return merge({}, state, action.response.entities);
}
return state;
}
Please see Redux real-world example for a demo of such approach.
React's update() immutability helper is a convenient way to create an updated version of a plain old JavaScript object without mutating it.
You give it the source object to be updated and an object describing paths to the pieces which need to be updated and changes that need to be made.
e.g., if an action had id and link properties and you wanted to push the link to an array of links in an item keyed with the id:
var update = require('react/lib/update')
// ...
return update(state, {
items: {
[action.id]: {
links: {$push: action.link}
}
}
})
(Example uses an ES6 computed property name for action.id)

Categories

Resources