I am doing some filtering using React Context and I am having some difficulty in updating a child's array value when a filter is selected.
I want to be able to filter by a minimum price, which is selected in a dropdown by the user, I then dispatch an action to store that in the reducers state, however, when I try and update an inner array (homes: []) that lives inside the developments array (which is populated with data on load), I seem to wipe out the existing data which was outside the inner array?
In a nutshell, I need to be able to maintain the existing developments array, and filter out by price within the homes array, I have provided a copy of my example code before, please let me know if I have explained this well enough!
export const initialState = {
priceRange: {
min: null
},
developments: []
};
// Once populated on load, the developments array (in the initialState object)
// will have a structure like this,
// I want to be able to filter the developments by price which is found below
developments: [
name: 'Foo',
location: 'Bar',
distance: 'xxx miles',
homes: [
{
name: 'Foo',
price: 100000
},
{
name: 'Bar',
price: 200000
}
]
]
case 'MIN_PRICE':
return {
...state,
priceRange: {
...state.priceRange,
min: action.payload
},
developments: [
...state.developments.map(development => {
// Something here is causing it to break I believe?
development.homes.filter(house => house.price < action.payload);
})
]
};
<Select onChange={event=>
dropdownContext.dispatch({ type: 'MIN_PRICE' payload: event.value }) } />
You have to separate homes from the other properties, then you can apply the filter and rebuild a development object:
return = {
...state,
priceRange: {
...state.priceRange,
min: action.payload
},
developments: state.developments.map(({homes, ...other}) => {
return {
...other,
homes: homes.filter(house => house.price < action.payload)
}
})
}
Related
I want to update age array
const [varient, setVarient] = useState([
{ id: 0, targets: { ageGender: { age: [], gender: [] }, parameterData } },
]);
After updating
Result
Array = [
{ id: 0, targets: { ageGender: { age: [12,13], gender: [] }, parameterData } },
]
If you want to update your state and the new value is computed based on the previous 'snapshot' (e.g. value) of your state, then you can pass a function to setState. This function takes in the previous snapshot and returns an updated value. This is the recommended approach by the React team. Read more about this concept in the documentation under subheading 'Functional updates'.
I have added a working codesandbox example to answer your question.
You could do something like this:
const addAge = (age: number) => {
const newVarient = [...varient];
newVarient[0].targets.ageGender.age.push(age);
setVarient(newVarient);
}
I have a possible infinite category tree and I would like to add, update or remove categories at any level with setState in react. I know this is possible with recursion but I don't have enough experience to manage this problem on my own. Here is how the data could possible look like:
const categories = [
{
id: "1",
name: "category1",
subCategories: [
{
id: "sub1",
name: "subcategory1",
subCategories: [
{ id: "subsub1", name: "subsubcategory1", subCategories: [] },
{ id: "subsub2", name: "subsubcategory2", subCategories: [] }
]
},
{ id: "sub2", name: "subcategory2", subCategories: [] }
]
},
{
id: "2",
name: "category2",
subCategories: []
}
]
Considering that your top level categories object is an object and not an array, the add and remove function could be the following (same pattern for update)
function add (tree, newCategory, parentId) {
if(tree.id === parentId)
return {
...tree,
subCategories: tree.subCategories.concat(newCategory)
}
return {
...tree,
subCategories: tree.subCategories.map(c => add(c, newCategory, parentId))
}
}
function remove (tree, idToRemove) {
if(tree.subCategories.map(c => c.id).includes(idToRemove))
return {
...tree,
subCategories: tree.subCategories.filter(c => c.id !== idToRemove)
}
return {
...tree,
subCategories: tree.subCategories.map(c => remove(c, idToRemove))
}
}
Prologue
To update a nested property in an immutable way, you need to copy or perform immutable operations on all its parents.
Setting a property on a nested object:
return {
...parent,
person: {
...parent.person,
name: 'New Name'
}
}
Arrays: You may pre-clone the array, or use a combination of .slice(), .map(), .filter() and the concatenation operator (...); Warning: .splice mutates the array.
(this can be a long topic, so I just gave a veryfast overview)
immer
As this can quickly get very ugly on objects with deep nesting, using the immer lib quite becomes a must at some point. Immer "creates new immutable state from mutations".
const newState = immer(oldState, draft => {
draft[1].subcategories[3].subcategories[1].name = 'Category'
})
In the extreme case, you can combine immer with lodash to create mutations in arbitrary places:
import set from 'lodash/set'
const newState = immer(oldState, draft => {
set(draft, [0, 'subcategories', 5, 'subcategories', 3], { id: 5, name:'Cat' })
})
"You might not need lodash" website has the recursive implementation for lodash/set. But, seriously, just use lodash.
PLUSES:
If you are using redux-toolkit, immer already is auto-applied on reducers, and is exposed as createNextState (check docs with care).
Deeply nested state can be an interesting use case for normalizr (long talk).
this is how the recursive function would look like.
the arguments:
id: id to look for
cats: the categories array to loop
nestSubCategory: (boolean) if we want to add the subcategory object in the subCategories array or not
subCategory: the category object we want to insert
const addCategories = (id, cats, nestSubCategory, subCategory)=> {
const cat = cats.find(item=> item.id === id)
const arrSubs = cats.filter(item => item.subCategories?.length)
.map(item => item.subCategories)
if(cat){
if(nestSubCategory){
cat.subCategories.push(subCategory)
return
}else{
cats.push(subCategory)
return
}
}
else{
return addCategories(id, arrSubs[0], nestSubCategory, subCategory)
}
}
addCategories("blabla1", categories, true, { id: "blabla2", name: "blablacategory1", subCategories: [] })
//console.log(categories)
console.log(
JSON.stringify(categories)
)
remember to update the object in the state replacing the entire categories array once the function is executed.
be careful with recursion 🖖🏽
you can do in a similar way to remove items, i leave to you the pleasure to experiment
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.
I'm (obviously) very new to React and Javascript, so apologies in advance if this is a stupid question. Basically I have an array of objects in this.state, each with its own nested array, like so:
foods: [
{
type: "sandwich",
recipe: ["bread", "meat", "lettuce"]
},
{
type: "sushi",
recipe: ["rice", "fish", "nori"]
}, ...
I've already written a function that maps through the state objects and runs .includes() on each object.recipe to see if it contains a string.
const newArray = this.state.foods.map((thing, i) => {
if (thing.recipe.includes(this.state.findMe)) {
return <p>{thing.type} contains {this.state.findMe}</p>;
} return <p>{this.state.findMe} not found in {thing.type}</p>;
});
The main issue is that .map() returns a value for each item in the array, and I don't want that. I need to have a function that checks each object.recipe, returns a match if it finds one (like above), but also returns a "No match found" message if NONE of the nested arrays contain the value it's searching for. Right now this function returns "{this.state.findMe} not found in {thing.type}" for each object in the array.
I do know .map() is supposed to return a value. I have tried using forEach() and .filter() instead, but I could not make the syntax work. (Also I can't figure out how to make this function a stateless functional component -- I can only make it work if I put it in the render() method -- but that's not my real issue here. )
class App extends React.Component {
state = {
foods: [
{
type: "sandwich",
recipe: ["bread", "meat", "lettuce"]
},
{
type: "sushi",
recipe: ["rice", "fish", "nori"]
},
{
type: "chili",
recipe: ["beans", "beef", "tomato"]
},
{
type: "padthai",
recipe: ["noodles", "peanuts", "chicken"]
},
],
findMe: "bread",
}
render() {
const newArray = this.state.foods.map((thing, i) => {
if (thing.recipe.includes(this.state.findMe)) {
return <p>{thing.type} contains {this.state.findMe}</p>;
} return <p>{this.state.findMe} not found in {thing.type}</p>;
});
return (
<div>
<div>
<h3>Results:</h3>
{newArray}
</div>
</div>
)
}
};
You could use Array.reduce for this:
const result = foods.reduce((acc,curr)=> curr.recipe.includes(findMe) ? `${curr.type} contains ${findMe}`:acc ,'nothing found')
console.log(result)
Though if the ingredient is found in more than one recipe it will only return the last one.
Alternatively, you could use a map and a filter:
const result = foods.map((thing, i) => {
if (thing.recipe.includes(findMe)) {
return `${thing.type} contains ${findMe}`;
}}).filter(val=>!!val)
console.log(result.length?result[0]:'nothing found')
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.