In a react component that uses state hooks to expose several state properties, is there a way to iterate through all of the state properties and potentially change them? The issue is that I have lots of state properties, so I'd rather not hard-code all the getters and setters to iterate over the state properties.
In this example, let's say that all of my state properties have a default of 0, and if they are different, I'd like to do something. How do I loop over the state properties?
const exampleComponent = () => {
const [prop1, setProp1] = React.useState(0);
const [prop2, setProp2] = React.useState(0);
const [prop3, setProp3] = React.useState(0);
//...etc., lots of properties
// Loop over the properties. How should this loop be written?
Object.keys(this.state).map(function (key) {
// do something with each key-value pair here
});
An alternative is to assign the states that you want into an array and then destructure them into named constants (if required) and enumerate the states array. See example below:
const exampleComponent = () => {
const states = [React.useState(0), React.useState(0), React.useState(0)];
const [
[prop1, setProp1],
[prop2, setProp2],
[prop3, setProp3],
] = states;
// Loop over the properties.
states.forEach(([state, setState]) => {
// do something with each key-value pair here
});
}
If you need to loop over the properties, I'd use an array for state instead:
const [numArr, setNumArr] = useState([0, 0, 0]);
// ...
numArr.forEach((num, i) => {
// do something with each key-value pair here
});
If you have lots of states that are related to each other, then instead of having each state separately, you might be better off using the useReducer hook.
EDIT:
Apologies, I should have mentioned this earlier that handling state with useReducer hook can be a bit verbose and may be complex if one is not familiar with it.
Here is an example, where instead of having three separate states, we have one state object with three properties, when UPDATE_ACTION1 is dispatched, the code loops over the properties and all the relevant ones are incremented by 2.
//define some actions
const UPDATE_ACTION1 = "UPDATE_ACTION1";
const UPDATE_ACTION2 = "UPDATE_ACTION2";
//define a reducer function that will update the state
const objReducer = (state, action) => {
switch (action.type) {
case UPDATE_ACTION1:
const keys = Object.keys(state);
const newState = {};
keys.forEach(key => {
//perform any function on each property/key
//here we just increment the value of each property by the given value
if (key !== "isValid") {
newState[key] = state[key] + action.value;
}
});
return newState;
case UPDATE_ACTION2:
//do something else e.g. check validity and return updated state
return { ...state, isValid: true };
default:
return state;
}
};
//inside the component: call useReducer and pass it the reducer function and an initial state
//it will return the current state and a dispatch function
const [objState, dispatch] = useReducer(objReducer, {
prop1: 0,
prop2: 0,
prop3: 0
});
//somewhere in your code, dispatch the action. it will update the state depending upon the action.
const somethingHappens = () => {
//some other operations are performed here
dispatch({ type: UPDATE_ACTION1, value: 2 });
};
Related
Is it legit to store data outside of a react component and change it from inside the component and derive state from it? This could help when updating complex deep-nested states.
import React from "react";
let globalState = {
foo: { bar: { baz: { value: 0 } } },
};
const Component = () => {
const [state, setState] = React.useState(globalState);
let baz = globalState.foo.bar.baz;
const changeValue = () => {
baz.value += 1;
setState({ ...globalState });
};
return (
<div>
<label>{state.foo.bar.baz.value}</label>
<button onClick={changeValue}>change</button>
</div>
);
};
Update:
My main intention of this aproach is to get a cleaner way of updating nested state properties. In my current component I use immer-produce to update nested values of the state, but even with produce it becomes extremely nasty when coming to nested state with arrays, indices etc.
This is an actual code snippet from my application:
const changeAction = useCallback((newAttrs) => {
setState(
produce((draft) => {
draft.newOrders[draft.selectedOrderIndex].nodes[
draft.selectedNodeIndex
].actions[draft.selectedActionIndex] = {
...draft.newOrders[draft.selectedOrderIndex].nodes[draft.selectedNodeIndex]
.actions[draft.selectedActionIndex],
...newAttrs,
};
})
);
}, [setState]);
Is there any other aproach to clean this up?
This sounds like something that would ideally be handled with the use of Context.
Using context can help you manage the state globally rather than mutating objects and passing them up and down the tree all the time, making them difficult to manage.
I have defined an object as follows in a file:
export const items = {
first: false,
second: false,
third: false
}
I'm using it in a component as follows:
import { items } from 'file';
const [elements, setElements] = useState(items);
I have a method which gets called when a button is clicked - the method should change the all the values in elements to true
Using the following changes the values but it does not trigger a re-render of the component (which is what I need)
Object.keys(elements).forEach(element => elements[element] = true);
How can I use setElements to update all the values in elements?
The problem you are facing is that you are mutating the state object, which means that at the end, prevState === nextState and React bails out of rendering. An option is to use a new object and copy the props like this, using the same combo of Object.keys and forEach but adding an extra step:
setState(prevState => {
const nextState = {}
Object.keys(prevState).forEach(key => {
nextState[key] = true
})
return nextState
})
I'm using Redux-toolkit with entityAdapter for state mangement in my React App. I want to apply table sorting for each column where my table rows are entities of Redux-toolkit entityAdapter. I want to change the sortComparer fuction like this in my reducer.
sortEntities: (state,action) =>{
return {
...state,
sortComparer:(a:myEntityType,b:myEntityType)=> a.title.localCompare(b.title)
};
},
I dispatch sortEntities action on onClick handler of columns. This sort of changing the sortComparer is not throwing any rules voilation error but not working. Can somebody help me?
What you are doing in the above code is storing a function to the sortComparer property of your state. You aren't actually applying the sort function anywhere, and it's not advisable to store non-serializable data like functions in Redux.
The adapter that you create by calling createEntityAdapter is an object which helps you interact with the state. The state itself is just an array of ids and a dictionary object of entities. The sortComparer property of the adapter is not part of the state, so it cannot be modified by modifying the state.
There are a lot of ways to approach this.
For example, you could select all entities of Redux and sort them locally.
const useSortedEntities = (sortBy) => {
// get an array of entities with the selector from the entity adapter
const allEntities = useSelector(selectEntities);
// sort based on the current property
// use ... to avoid mutation
// would probably want to memoize this
return [...allEntities].sort(
(a, b) => a[sortBy].localCompare(b[sortBy])
);
}
const SomeComponent = () => {
const [sortProperty, setSortProperty] = useState('author');
const sortedList = useSortedEntities(sortProperty);
...
Or you could dispatch an action with the sort property in the payload and save the sort property in Redux. You can then use createSelector to create a selector for the sorted data.
const mySlice = createSlice({
name: 'someName',
initialState: myAdapter.getInitialState({
// additional properties to include in initial state
sortBy: 'author'
}),
reducers: {
sortEntities: (state, action: PayloadAction<string>) => {
state.sortBy = action.payload;
}
...
const selectSortedEntities = createSelector(
// first select entities with the selector from the adapter
selectEntities,
// next select the sort order
(state: RootState) => state.pathToSlice.sortBy
// then combine them to return a sorted array
(allEntities, sortBy) => [...allEntities].sort(
(a, b) => a[sortBy].localCompare(b[sortBy])
);
)
const SomeComponent = () => {
const sortedList = useSelector(selectSortedEntities);
const dispatch = useDispatch();
const onClick = () => {
dispatch(sortEntities('title'));
}
...
I have a redux application with a "campaign" reducer/store.
Currently I have repeated code to check if a specific campaign is loaded or needs an API call to fetch details from the DB. Much simplified it looks like this:
// Reducer ----------
export default campaignReducer => (state, action) {
const campaignList = action.payload
return {
items: {... campaignList}
}
}
// Component ----------
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: campaign.id
&& campaign.meta
&& (campaign.meta.loaded || campaign.meta.loading),
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);
Now I don't like to repeat the complex condition for needFetch. I also don't like to have this complex code in the mapStateToProps function at all, I want to have a simple check. So I came up with this solution:
// Reducer NEW ----------
const needFetch = (items) => (id) => { // <-- Added this function.
if (!items[id]) return true;
if (!items[id].meta) return true;
if (!items[id].meta.loaded && !items[id].meta.loading) return true;
return false;
}
export default campaignReducer => (state, action) {
const campaignList = action.payload
return {
needFetch: needFetch(campaignList), // <-- Added public access to the new function.
items: {... campaignList}
}
}
// Component NEW ----------
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: state.campaign.needFetch(campaignId), // <-- Much simpler!
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);
Question: Is this a good solution, or does the redux-structure expect a different pattern to solve this?
Question 2: Should we add getter methods to the store, like store.campaign.getItem(myId) to add sanitation (make sure myId exists and is loaded, ..) or is there a different approach for this in redux?
Usually computational components should be responsible for doing this type of logic. Sure your function has a complex conditional check, it belongs exactly inside your computational component (just like the way you currently have it).
Also, redux is only for maintaining state. There's no reason to add methods to query values of the current state inside your reducers. A better way would be having a module specifically for parsing your state. You can then pass state to the module and it would extract the relevant info. Keep your redux/store code focused on computing a state only.
Your approach is somewhat against the idiomatic understanding of state in redux. You should keep only serializable data in the state, not functions. Otherwise you loose many of the benefits of redux, e.g. that you can very easily stash your application's state into the local storage or hydrate it from the server to resume previous sessions.
Instead, I would extract the condition into a separate library file and import it into the container component where necessary:
// needsFetch.js
export default function needsFetch(campaign) {
return campaign.id
&& campaign.meta
&& (campaign.meta.loaded || campaign.meta.loading);
}
// Component ----------
import needsFetch from './needsFetch';
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: needsFetch(campaign),
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);
I need access to user editable state from two or more reducers. Is there a way to access state controlled by another reducer without passing it to the reducer through the action's payload? I want to avoid having every action send user settings to reducers.
State
{
userSettings: {
someSetting: 5
},
reducer1State: {
someValue: 10 // computed with userSettings.someSetting
},
reducer2State: {
someOtherValue: 20 // computed with userSettings.someSetting
}
}
From the reducer1 I would like to get at userSettings.someSetting using something like the following:
function update(state={}, action) {
if (action.type === constants.REDUCER_1.CALCULATE) {
return _.assign({}, state, {
someValue: 2 * GETSTATE().userSettings.someSetting
});
}
...
I do not want to have to send userSettings from the action like this:
export function calculate(userSettings) {
return {
type: constants.REDUCER_1.CALCULATE,
userSettings: userSettings
};
}
One of the golden rules of Redux is that you should try to avoid putting data into state if it can be calculated from other state, as it increases likelihood of getting data that is out-of-sync, e.g. the infamous unread-messages counter that tells you that you have unread messages when you really don't.
Instead of having that logic in your reducer, you can use Reselect to create memoized selectors that you use in your connectStateToProps function, to get your derived data, e.g. something along the line of this:
const getSomeSettings = state => state.userSettings.someSetting;
const getMultiplier = state => state.reducer1.multiplier;
const getSomeValue = createSelector([getSomeSettings, getMultiplier],
(someSetting, multiplier) => {
});
const mapStateToProps(state) => {
return {
someValue: getSomeValue(state)
}
}
const MyConnectedComponent = connect(mapStateToProps)(MyComponent);