I have the following reducer in React Redux:
export const reducer = (state = initialStateData, action) => {
switch (action.type) {
case Action.TOGGLE_ARR_FILTER:
{
const subArr = state.jobOffers.filters[action.key];
const filterIdx = subArr.indexOf(action.value[0]);
const newArr = { ...state.jobOffers.filters
};
if (filterIdx !== -1) {
newArr[action.key].splice(filterIdx, 1);
} else {
newArr[action.key].push(action.value[0]);
}
return {
...state,
jobOffers: {
...state.jobOffers,
filters: {
...newArr,
},
},
};
}
And this is my object:
const initialStateData = {
jobOffers: {
filters: {
employments: [],
careerLevels: [],
jobTypeProfiles: [],
cities: [],
countries: [],
},
configs: {
searchTerm: '',
currentPage: 1,
pageSize: 5,
},
},
};
The reducer as such seems to work, it toggles the values correctly.
But: Redux always shows "states are equal", which is bad as it won't recognize changes.
Can someone help ? I assume that I am returning a new object..
you can use Immer , redux also uses this for immutable updates for nested stuffs.
Because of this, you can write reducers that appear to "mutate" state, but the updates are actually applied immutably.
const initialStateData = {
jobOffers: {
filters: {
employments: [],
careerLevels: [],
jobTypeProfiles: [],
cities: [],
countries: [],
},
configs: {
searchTerm: '',
currentPage: 1,
pageSize: 5,
},
},
};
export const reducer = immer.produce((state = initialStateData, action) => {
switch (action.type) {
case Action.TOGGLE_ARR_FILTER:
const subArr = state.jobOffers.filters[action.key];
const filterIdx = subArr.indexOf(action.value[0]);
const newArr = state.jobOffers.filters;
if (filterIdx !== -1)
newArr[action.key].splice(filterIdx, 1);
else
newArr[action.key].push(action.value[0]);
return state;
}
})
Although you take a copy of state.jobOffers.filters, this still holds references to original child arrays like employments. So when you mutate newArr[action.key] with splice or push, Redux will not see that change, as it is still the same array reference.
You could replace this:
if (filterIdx !== -1) {
newArr[action.key].splice(filterIdx, 1);
} else {
newArr[action.key].push(action.value[0]);
}
with:
newArr[action.key] = filterIdx !== -1
? [...newArr[action.key].slice(0, filterIdx), ...newArr[action.key].slice(filterIdx+1)]
: [...newArr[action.key], action.value[0]]);
BTW, you don't have to copy newArr again, as it already is a copy of filters. You can replace:
filters: {
...newArr,
},
by just:
filters: newArr,
Related
I have this Redux store and reducer
const INITIAL_WORK = {
departments: []
}
const works = (state = INITIal_WORK, action) => {
switch(action.type){
case 'ADD_DEPARTMENT':
return {
...state,
departments: [...state.department, action.item]
}
default:
return state
}
}
In departments work people so I want to this people was inside a single department works people. So after fetch data from db I want my store look like this:
const INITIAL_WORK = {
departments: [
{
id: 1,
name: "First department",
people: [
{
id: 1,
name: "Johna Wayne"
},
{
id: 2,
name: "Jessica Biel"
},
{
id: 3,
name: "Bratt Pitt"
}
]
},
{
id: 2,
name: "second department",
people: [
{
id: 4,
name: "Salma Hayek"
},
{
id: 5,
name: "Sylvester Stallone"
}
]
}
]
}
Is it possible create case in reducer which will be added people inside people array inside single department? How can I do that?
Yes, absolutely.
Let us assume your API returns payload as following.
{
departmentId: `id of the department`
id: `person id`
name: `person name`
}
const INITIAL_WORK = {
departments: []
}
const works = (state = INITIal_WORK, action) => {
switch(action.type){
case 'ADD_DEPARTMENT':
return {
...state,
departments: [...state.department, action.item]
}
case 'ADD_PERSON':
const { departmentId, id, name } = action.item
const departments = [...state.departments];
departments[departmentId].people.push({id, name})
return {
...state,
departments
}
default:
return state
}
}
Combining Immer.js and Redux will be helpful for this case.
Here is a simple example of the difference that Immer could make in practice.
// Reducer with inital state
const INITAL_STATE = {};
// Shortened, based on: https://github.com/reactjs/redux/blob/master/examples/shopping-cart/src/reducers/products.js
const byId = (state = INITAL_STATE, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
return {
...state,
...action.products.reduce((obj, product) => {
obj[product.id] = product
return obj
}, {})
}
default:
return state
}
}
After using Immer, our reducer can be expressed as:
import produce from "immer"
// Reducer with inital state
const INITAL_STATE = {};
const byId = produce((draft, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
action.products.forEach(product => {
draft[product.id] = product
})
}
}, INITAL_STATE)
let mergePeople=[];
INITIAL_WORK.departments.filter((cur,index)=>{
cur.people.filter((person)=>{
mergePeople.push(person);
})
}
)
console.log(mergePeople);
I have the following code to update the currentScore of a rubricItem object. This works fine.
case SAVE_SCORELIST_SUCCESS:
const scoreItem = action.payload.scoreItem;
return {
...state,
loading: false,
editing: false,
rubricItems: {
...state.rubricItems,
[scoreItem.rubricItemId]: {
...state.rubricItems[scoreItem.rubricItemId],
currentScore: scoreItem.currentScore,
}
}
};
However, I may receive an array object holding scores for multiple rubricItems instead of updating a single rubricItem with a single scorItem as I did above.
I know I can use .map() to iterate through the array:
scoreItems.map(si=>{})
But, I do not know how I can integrate it into this:
case SAVE_SCORELIST_SUCCESS:
const scoreItems = action.payload.scoreItems;
return {
...state,
loading: false,
editing: false,
rubricItems: {
...state.rubricItems,
[scoreItems[x].rubricItemId]: {
...state.rubricItems[scoreItems[x].rubricItemId],
currentScore: scoreItems[x].currentScore,
}
}
};
Any ideas?
You can try this:
First you need to iterate over scoreItems and make a map object of updated score items.
Once you have done that, you can use the spread operator with the current score items in state.
case SAVE_SCORELIST_SUCCESS:
let updatedScoreItems = {};
action.payload.scoreItem.forEach(scoreitem => {
updatedScoreItems[scoreItem.rubricItemId] = {
...state.rubricItems[scoreItem.rubricItemId],
currentScore: scoreItem.currentScore,
}
})
return {
...state,
loading: false,
editing: false,
rubricItems: {
...state.rubricItems,
...updatedScoreItems
}
};
Instead of mapping over scoreItem, map over the rubricItems which will be cleaner.
const updatedRubricItems = items.rubricItems.map(rubricItem => {
const scoreForRubric = scoreItems.find(si => si.rubricItemId === rubricItem.id);// i assume you have some id for your rubric item
if(scoreForRubric){
return {...rubricItem, currentScore: scoreForRubric.currentScore}
}else {
return rubricItem
}
});
return {
...state,
loading: false,
editing: false,
rubricItems: updatedRubricItems
};
I'm trying some app in react redux and i have a problem with updating (push, remove, update) the nested array in state.
I have some object called service like this:
{
name: 'xzy',
properties: [
{ id: 1, sName: 'xxx'},
{ id: 2, sName: 'zzz'},
]
}
Whatever I did (in case of adding property to collection) in the reducer with the properties collection generate problem that all properties got same values as the last I had recently added -> Added property object is in service properties collection but the action replace all values in all properties in this collection.
My reducer:
export function service(state = {}, action) {
switch (action.type) {
case 'ADD_NEW_PROPERTY':
console.log(action.property) // correct new property
const service = {
...state, properties: [
...state.properties, action.property
]
}
console.log(service); // new property is pushed in collection but all properties get same values
return service
default:
return state;
}
}
I have tried some solution with immutability-helper library and it generate the same problem:
export function service(state = {}, action) {
case 'ADD_NEW_PROPERTY':
return update(state, {properties: {$push: [action.property]}})
default:
return state;
}
For example when I add new property { id: 1, sName: 'NEW'} to example above I will get this state:
{
name: 'xzy',
properties: [
{ id: 1, sName: 'NEW'},
{ id: 1, sName: 'NEW'},
{ id: 1, sName: 'NEW'}
]
}
Can someone help? :)
Make a copy of action.property as well. Whatever is dispatching this action, it could be reusing the same object.
export function service(state = {}, action) {
switch (action.type) {
case 'ADD_NEW_PROPERTY':
console.log(action.property) // correct new property
const service = {
...state,
properties: [
...state.properties,
{ ...action.property }
]
}
console.log(service); // new property is pushed in collection but all properties get same values
return service
default:
return state;
}
}
I'd recommend you to use Immutable data https://facebook.github.io/immutable-js/docs/#/List
import { fromJS, List } from 'immutable';
const initialState = fromJS({
propeties: List([{ id: 1, sName: 'xyz' }]
}
function reducer(state = initialState, action) {
case ADD_NEW_PROPERTY:
return state
.update('properties', list => list.push(action.property));
// ...
}
Your service reducer should probably look somewhat like this:
// Copy the state, because we're not allowed to overwrite the original argument
const service = { ...state };
service.properties.append(action.property)
return service
You should always copy the state before returning it.
export default function(state = {}, action) {
switch(action.type) {
case 'GET_DATA_RECEIVE_COMPLETE': {
const data = action.firebaseData;
const newState = Object.assign({}, state, {
data
});
return newState
}
default:
return state;
}
}
I'm trying to update array inside object in my reducer.
const initialState = {
group: {
name: "",
date: "",
description: "",
users: [],
posts: []
},
morePosts: false,
groups: []
};
export function groups(state = initialState, action) {
switch (action.type) {
.......
case REQUEST_MORE_POSTS:
{
return {
...state,
group:{
...state.group,
posts: [
...state.group.posts,
...action.payload.posts
]
},
morePosts: action.payload.morePosts
}
}
case ADD_NEW_POST:
{
return {
...state,
group:{
...state.group,
posts: [
action.payload,
...state.group.posts
]
}
}
}
........
default:
return state;
}
}
Unfortunately in both cases I get an error:
It works when I extract posts out of my group object but I need it inside.
I can't figure out what I've done wrong here. Can someone point me to the right direction?
Here is an action creator for adding new post.
export function addPost(url, payload) {
return function(dispatch) {
axios.post(url + "php/addPostGroup.php", {payload}).then(response => {
dispatch({
type: ADD_NEW_POST,
payload: response.data.post
})
})
}
}
response.data.post is a simple object.
I've added console.log() before dispatch. This is how my response looks like:
Alright I solved it. Before fetching any data my state.group.posts array was for some reason treated as undefined. I had to manually declare it as empty array using
var posts = state.group.posts != undefined ? state.group.posts:[];
The relevant Redux state consists of an array of objects representing layers.
Example:
let state = [
{ id: 1 }, { id: 2 }, { id: 3 }
]
I have a Redux action called moveLayerIndex:
actions.js
export const moveLayerIndex = (id, destinationIndex) => ({
type: MOVE_LAYER_INDEX,
id,
destinationIndex
})
I would like the reducer to handle the action by swapping the position of the elements in the array.
reducers/layers.js
const layers = (state=[], action) => {
switch(action.type) {
case 'MOVE_LAYER_INDEX':
/* What should I put here to make the below test pass */
default:
return state
}
}
The test verifies that a the Redux reducer swaps an array's elements in immutable fashion.
Deep-freeze is used to check the initial state is not mutated in any way.
How do I make this test pass?
test/reducers/index.js
import { expect } from 'chai'
import deepFreeze from'deep-freeze'
const id=1
const destinationIndex=1
it('move position of layer', () => {
const action = actions.moveLayerIndex(id, destinationIndex)
const initialState = [
{
id: 1
},
{
id: 2
},
{
id: 3
}
]
const expectedState = [
{
id: 2
},
{
id: 1
},
{
id: 3
}
]
deepFreeze(initialState)
expect(layers(initialState, action)).to.eql(expectedState)
})
One of the key ideas of immutable updates is that while you should never directly modify the original items, it's okay to make a copy and mutate the copy before returning it.
With that in mind, this function should do what you want:
function immutablySwapItems(items, firstIndex, secondIndex) {
// Constant reference - we can still modify the array itself
const results= items.slice();
const firstItem = items[firstIndex];
results[firstIndex] = items[secondIndex];
results[secondIndex] = firstItem;
return results;
}
I wrote a section for the Redux docs called Structuring Reducers - Immutable Update Patterns which gives examples of some related ways to update data.
You could use map function to make a swap:
function immutablySwapItems(items, firstIndex, secondIndex) {
return items.map(function(element, index) {
if (index === firstIndex) return items[secondIndex];
else if (index === secondIndex) return items[firstIndex];
else return element;
}
}
In ES2015 style:
const immutablySwapItems = (items, firstIndex, secondIndex) =>
items.map(
(element, index) =>
index === firstIndex
? items[secondIndex]
: index === secondIndex
? items[firstIndex]
: element
)
There is nothing wrong with the other two answers, but I think there is even a simpler way to do it with ES6.
const state = [{
id: 1
}, {
id: 2
}, {
id: 3
}];
const immutableSwap = (items, firstIndex, secondIndex) => {
const result = [...items];
[result[firstIndex], result[secondIndex]] = [result[secondIndex], result[firstIndex]];
return result;
}
const swapped = immutableSwap(state, 2, 0);
console.log("Swapped:", swapped);
console.log("Original:", state);