Applying mutations to nested Maps and DRY - javascript

We've been developing a SPA using React+Redux+Immutable JS.
Our Redux state contains 17 Maps (at the moment, but it's promised to grow) with exactly the same structure {page, pages, sortBy, items,...} and we'd like to avoid copy-pasting reducers (and tests), a trivial example is here:
state = fromJS({
pageA: {
tableA1: {loading:..., page:...., pages:..., items:...},
tableA2: {loading:..., page:...., pages:..., items:...}
},
pageB: {
tableB1: {loading:..., page:...., pages:..., items:...},
tableB2: {loading:..., page:...., pages:..., items:...}
}
});
// reducers:
setLoadingTableA1 = state => state.setIn(['pageA', 'tableA1', 'loading'], true);
setLoadingTableA2 = state => state.setIn(['pageA', 'tableA2', 'loading'], true);
setLoadingTableB1 = state => state.setIn(['pageB', 'tableB1', 'loading'], true);
setLoadingTableB2 = state => state.setIn(['pageB', 'tableB2', 'loading'], true);
Obviously, all four reducers can be replaced with reusable one, e.g.:
composeSetLoading = selector => state => selector(state).set('loading', true);
usage:
selectPageATable1 = state => state.getIn(['pageA', 'tableA1']);
// ^^^ It works only for mapping data from state to props
// Unfortunately, it can't be used for composing reducers
setLoadingTableA1 = composeSetLoading(selectPageATable1);
Other benefits: composeSetLoading is needed to be tested once. setLoadingTableA1 is reusable at #connect as well:
#connect(state => ({loading : selectPageATable1(state).get('loading')}))
class TableA1 extends React.Component {...}
Is there a possibility to implement that? I would appreciate any advice/idea.
Update I:
I know about Map.updateIn(..., updater);, it's different. It allows to move out state => state.set('loadding', true), but we're seeking for a way to move out selector
Update II:
I've tried to play with Map.updateIn(..., updater); unfortunately, it's not good enough, as updater should be provided with old state and some data in most cases... so the only way is to create a new function every time:
setItemsTableA1 = (state, items) => state.updateIn(['pageA','tableA1'], state => state.set('items', items));

Related

Cannot read property 'length' of undefined in functional component while it works in class component [duplicate]

I'm finding these two pieces of the React Hooks docs a little confusing. Which one is the best practice for updating a state object using the state hook?
Imagine a want to make the following state update:
INITIAL_STATE = {
propA: true,
propB: true
}
stateAfter = {
propA: true,
propB: false // Changing this property
}
OPTION 1
From the Using the React Hook article, we get that this is possible:
const [count, setCount] = useState(0);
setCount(count + 1);
So I could do:
const [myState, setMyState] = useState(INITIAL_STATE);
And then:
setMyState({
...myState,
propB: false
});
OPTION 2
And from the Hooks Reference we get that:
Unlike the setState method found in class components, useState does
not automatically merge update objects. You can replicate this
behavior by combining the function updater form with object spread
syntax:
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
As far as I know, both works. So, what is the difference? Which one is the best practice? Should I use pass the function (OPTION 2) to access the previous state, or should I simply access the current state with spread syntax (OPTION 1)?
Both options are valid, but just as with setState in a class component you need to be careful when updating state derived from something that already is in state.
If you e.g. update a count twice in a row, it will not work as expected if you don't use the function version of updating the state.
const { useState } = React;
function App() {
const [count, setCount] = useState(0);
function brokenIncrement() {
setCount(count + 1);
setCount(count + 1);
}
function increment() {
setCount(count => count + 1);
setCount(count => count + 1);
}
return (
<div>
<div>{count}</div>
<button onClick={brokenIncrement}>Broken increment</button>
<button onClick={increment}>Increment</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
If anyone is searching for useState() hooks update for object
Through Input
const [state, setState] = useState({ fName: "", lName: "" });
const handleChange = e => {
const { name, value } = e.target;
setState(prevState => ({
...prevState,
[name]: value
}));
};
<input
value={state.fName}
type="text"
onChange={handleChange}
name="fName"
/>
<input
value={state.lName}
type="text"
onChange={handleChange}
name="lName"
/>
Through onSubmit or button click
setState(prevState => ({
...prevState,
fName: 'your updated value here'
}));
The best practice is to use separate calls:
const [a, setA] = useState(true);
const [b, setB] = useState(true);
Option 1 might lead to more bugs because such code often end up inside a closure which has an outdated value of myState.
Option 2 should be used when the new state is based on the old one:
setCount(count => count + 1);
For complex state structure consider using useReducer
For complex structures that share some shape and logic you can create a custom hook:
function useField(defaultValue) {
const [value, setValue] = useState(defaultValue);
const [dirty, setDirty] = useState(false);
const [touched, setTouched] = useState(false);
function handleChange(e) {
setValue(e.target.value);
setTouched(true);
}
return {
value, setValue,
dirty, setDirty,
touched, setTouched,
handleChange
}
}
function MyComponent() {
const username = useField('some username');
const email = useField('some#mail.com');
return <input name="username" value={username.value} onChange={username.handleChange}/>;
}
Which one is the best practice for updating a state object using the state hook?
They are both valid as other answers have pointed out.
what is the difference?
It seems like the confusion is due to "Unlike the setState method found in class components, useState does not automatically merge update objects", especially the "merge" part.
Let's compare this.setState & useState
class SetStateApp extends React.Component {
state = {
propA: true,
propB: true
};
toggle = e => {
const { name } = e.target;
this.setState(
prevState => ({
[name]: !prevState[name]
}),
() => console.log(`this.state`, this.state)
);
};
...
}
function HooksApp() {
const INITIAL_STATE = { propA: true, propB: true };
const [myState, setMyState] = React.useState(INITIAL_STATE);
const { propA, propB } = myState;
function toggle(e) {
const { name } = e.target;
setMyState({ [name]: !myState[name] });
}
...
}
Both of them toggles propA/B in toggle handler.
And they both update just one prop passed as e.target.name.
Check out the difference it makes when you update just one property in setMyState.
Following demo shows that clicking on propA throws an error(which occurs setMyState only),
You can following along
Warning: A component is changing a controlled input of type checkbox to be uncontrolled. Input elements should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.
It's because when you click on propA checkbox, propB value is dropped and only propA value is toggled thus making propB's checked value as undefined making the checkbox uncontrolled.
And the this.setState updates only one property at a time but it merges other property thus the checkboxes stay controlled.
I dug thru the source code and the behavior is due to useState calling useReducer
Internally, useState calls useReducer, which returns whatever state a reducer returns.
https://github.com/facebook/react/blob/2b93d686e3/packages/react-reconciler/src/ReactFiberHooks.js#L1230
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
...
try {
return updateState(initialState);
} finally {
...
}
},
where updateState is the internal implementation for useReducer.
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
currentHookNameInDev = 'useReducer';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return updateReducer(reducer, initialArg, init);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
If you are familiar with Redux, you normally return a new object by spreading over previous state as you did in option 1.
setMyState({
...myState,
propB: false
});
So if you set just one property, other properties are not merged.
One or more options regarding state type can be suitable depending on your usecase
Generally you could follow the following rules to decide the sort of state that you want
First: Are the individual states related
If the individual state that you have in your application are related to one other then you can choose to group them together in an object. Else its better to keep them separate and use multiple useState so that when dealing with specific handlers you are only updating the relavant state property and are not concerned about the others
For instance, user properties such as name, email are related and you can group them together Whereas for maintaining multiple counters you can make use of multiple useState hooks
Second: Is the logic to update state complex and depends on the handler or user interaction
In the above case its better to make use of useReducer for state definition. Such kind of scenario is very common when you are trying to create for example and todo app where you want to update, create and delete elements on different interactions
Should I use pass the function (OPTION 2) to access the previous
state, or should I simply access the current state with spread syntax
(OPTION 1)?
state updates using hooks are also batched and hence whenever you want to update state based on previous one its better to use the callback pattern.
The callback pattern to update state also comes in handy when the setter doesn't receive updated value from enclosed closure due to it being defined only once. An example of such as case if the useEffect being called only on initial render when adds a listener that updates state on an event.
Both are perfectly fine for that use case. The functional argument that you pass to setState is only really useful when you want to conditionally set the state by diffing the previous state (I mean you can just do it with logic surrounding the call to setState but I think it looks cleaner in the function) or if you set state in a closure that doesn't have immediate access to the freshest version of previous state.
An example being something like an event listener that is only bound once (for whatever reason) on mount to the window. E.g.
useEffect(function() {
window.addEventListener("click", handleClick)
}, [])
function handleClick() {
setState(prevState => ({...prevState, new: true }))
}
If handleClick was only setting the state using option 1, it would look like setState({...prevState, new: true }). However, this would likely introduce a bug because prevState would only capture the state on initial render and not from any updates. The function argument passed to setState would always have access to the most recent iteration of your state.
Both options are valid but they do make a difference.
Use Option 1 (setCount(count + 1)) if
Property doesn't matter visually when it updates browser
Sacrifice refresh rate for performance
Updating input state based on event (ie event.target.value); if you use Option 2, it will set event to null due to performance reasons unless you have event.persist() - Refer to event pooling.
Use Option 2 (setCount(c => c + 1)) if
Property does matter when it updates on the browser
Sacrifice performance for better refresh rate
I noticed this issue when some Alerts with autoclose feature that should close sequentially closed in batches.
Note: I don't have stats proving the difference in performance but its based on a React conference on React 16 performance optimizations.
I find it very convenient to use useReducer hook for managing complex state, instead of useState. You initialize state and updating function like this:
const initialState = { name: "Bob", occupation: "builder" };
const [state, updateState] = useReducer(
(state, updates) => {...state, ...updates},
initialState
);
And then you're able to update your state by only passing partial updates:
updateState({ occupation: "postman" })
The solution I am going to propose is much simpler and easier to not mess up than the ones above, and has the same usage as the useState API.
Use the npm package use-merge-state (here). Add it to your dependencies, then, use it like:
const useMergeState = require("use-merge-state") // Import
const [state, setState] = useMergeState(initial_state, {merge: true}) // Declare
setState(new_state) // Just like you set a new state with 'useState'
Hope this helps everyone. :)

How can I get the updated state with redux

I have a react project and redux for state management. These are my actions.
const mapDispatchToProps = dispatch => ({
handleChange: (name, value) => { dispatch(handleChange(name, value)) },
filterRooms: (rooms) => { dispatch(filterRooms(rooms)) }
});
I have to use these 2 method one by one.
this.props.handleChange(pets, true); // handle changes
this.filterRooms(); // filtering with changing value
filterRooms = () => {
let {rooms,pets} = this.props; // Here I should get pets === true but it's false.
// filter by pets
if (pets) {
tempRooms = tempRooms.filter(room => room.pets === true);
}
this.props.filterRooms(tempRooms);
}
If I use setTimeout for second method thats ok but I think that's not a correct way.
this.props.handleChange(name, value);
setTimeout(() => {
this.filterRooms();
}, 500);
Seems that below two function run in sequence, one after another
this.props.handleChange(pets, true); // handle changes
this.filterRooms(); // filtering with changing value
First dispatch changed value to Redux. And it is updated there (as sample with setTimeout works). But don't expect updated value from Redux will be immediately available to this.filterRooms().
You have some Reactjs component. Reactjs component is essentially class or function. Then you wrap in in connect. So your code may look like this
class Component1 extends React.Component {
onChange: () => {
this.props.handleChange(pets, true); // handle changes
this.filterRooms(); // filtering with changing value
}
// other staff
}
export default connect(mapStateToProps, mapDispatchToProps)(Component1)
Whats happens in React. It instantiate class Component1, then calls connect which in turn calls some methods of your class (i.e. render() or something else). connect also pass some values from Redux store to your component as props. Method of Component1 is executed and may change Redux state (as it do in your sample). But updated props will not be immediately available. props are just arguments, that have been passed by connect function. To change arguments of any function, you should call it once again. So after receiving updated pets, connect will call your component again with updated pets. But it will be later.
connect() -> calls Component1 and passes props.pets = false -> Compoentn1 sets pets in Redux to true -> connect() receives updated pets and calls Component1 with props.pets = true
That's why trick with setTimeout works. Ste timeout just await for second call of Component1
To solve your exact issue, don't read pets from props if you know that you've updated it.
this.props.handleChange(pets, true); // handle changes
this.filterRooms(true); // filtering with changing value
filterRooms = (pets) => {
let {rooms} = this.props;
// filter by pets
if (pets) {
tempRooms = tempRooms.filter(room => room.pets === true);
}
this.props.filterRooms(tempRooms);
}

When using useState hook - Is there an importance to changing the setState functions call order?

I have a React functional component with two state variables (itemsData & itemsCollections). The variables are updated in the useEffect method. But after useEffect occur one of the state variables is null.
Upon switching the setStateFunctions (setItemsData & setItemsCollect) call order both arguments are inialized as expected.
How's that?
const MyComponent = ({itemsIds}) => {
const [itemsData, setItemsData] = useState([]);
const [itemsCollections, setItemsCollect] = useState({});
useEffect(() => {
fetchItemsData({ itemsIds }).then(({ items, itemCollect }) => {
setItemsData(items);
setItemsCollect(itemCollect);
})
}, [itemsIds]);
...
console.log('itemsData', itemsData) // the expected array
console.log('itemCollect', itemCollect) // empty objecy
State after useEffect: itemCollect = {}, itemsData = [{value:...},...]
Switching the order of the calls:
const MyComponent = ({itemsIds}) => {
...
useEffect(() => {
fetchItemsData({ itemsIds }).then(({ items, itemCollect }) => {
setItemsCollect(itemCollect); // <--> switched rows
setItemsData(items); // <--> switched rows
})
}, [itemsIds]);
...
console.log('itemsData', itemsData) // the expected array
console.log('itemCollect', itemCollect) // the expected object
State after useEffect: itemCollect = { someValue: ...} , itemsData = [{value:...},...]
There is a performance optimization called batching, which can change between React versions. When this optimization is applied, multiple setState calls will be batched together before the next render (and the order does not matter).
When not applied (e.g. inside a Promise as in your case, see Does React batch state update functions when using hooks?), then each state update will trigger a new render (and the order matters).
=> console.log('itemCollect', itemCollect) may log different data in each render.
If you need to force a single state update, then calling a single dispatch from useReducer might be the best option.

Stateless functional component method never gets fresh data from Redux store, but others do

I got into some trouble.
I have a pretty common functional component connected to Redux via mapStateToProps:
const TableWrapper = ({ studentsSelection }) => {
const onComponentStateChanged = params =>
params.api.forEachNode(node =>
studentsSelection.selectedStudents.findIndex(student =>
student.number === node.data.number) >= 0 &&
node.setSelected(true)
);
const gridOptions = getGridOptions(onComponentStateChanged(), onSelection);
return (
<BootStrapTableWrapper>
<Table gridOptions={gridOptions} />
</BootStrapTableWrapper>
);
};
TableWrapper.propTypes = {
studentsSelection: PropTypes.shape({
selectedStudents: PropTypes.array
})
};
const mapStateToProps = state => ({
studentsSelection: state.gpCreationWizard.studentsSelection
});
export default connect(mapStateToProps)(TableWrapper);
Where getGridOption refers to this:
const getStudentsSelectionGridOptions = (onComponentStateChanged, updateStudentsSelectionSelectionAction) => ({
...studentsSelectionGridOptions,
onComponentStateChanged(props) {
onComponentStateChanged(props);
},
onRowSelected({node}) {
updateStudentsSelectionSelectionAction(node);
},
});
export default getStudentsSelectionGridOptions;
And resulted gridOptions are used for Ag-Grid table.
So let me explain the business function: I'm using Ag-Grid table, and when user select some students, they are added to the Redux store. Then I'm using the onComponentStateChanged to keep the selection on pagination. So if user changed the page, and the old data will be replaced by new one, I want the selection to be kept when he's gonna return back. But the problem is that onComponentStateChanged is always referring to the same studentsSelection.selectedStudents (but other methods and render receiving new data). So this is probably something about js scopes. Please, what should I do to make this function be using the new data, not the old one. I tried to wrap that one in anther function, but that didn't have any effect. The funny fact, that this works fine for class component and this.props referring. If I explained that not very clear, please ask about more details.
not sure what getGridOptions does... but I think the problem is, you are calling the method instead of passing the method, so change:
const gridOptions = getGridOptions(onComponentStateChanged(), onSelection);
to:
const gridOptions = getGridOptions(onComponentStateChanged, onSelection);
so onComponentStateChanged is sent as method and not the result of calling it

Redux reducer - on state change

I want to save the current state of a specific reducer into session storage without explicitly calling it on every action.
const { handleActions } = require("redux-actions");
// How do i make this logic run on any state change without adding it to every action?
const onStateChange = (state) => sessionStorage.save(keys.myReducer, state);
const myReducer = handleActions
({
[Actions.action1]: (state, action) => (
update(state, {
prop1: {$set: action.payload},
})
),
[Actions.action2]: (state, action) => (
update(state, {
prop2: {$set: action.payload},
})
)
},
//****************INITIAL STATE***********************
{
prop1: [],
prop2: []
}
);
Is there a way to catch the state change event for a specific reducer?
I know of store.subscribe, but that's for the entire store, i'm talking about listening to a specific reducer change
Thanks
Unfortunately you can't watch specific parts of your store because your store is actually an only reducer (combineReducers return a single big reducer). That said you can manually check if your reducer has changed by doing state part comparison.
let previousMyReducer = store.getState().myReducer;
store.subscribe(() => {
const { myReducer } = store.getState();
if (previousMyReducer !== myReducer) {
// store myReducer to sessionStorage here
previousMyReducer = myReducer;
}
})
You can use redux-saga to listen for specific actions and persist the store (and do your other logice) when they are fired. This seems like a good fit for your needs.
It's not exactly tied to specific store changes, but rather to a set of actions. However, I think this model (listening for actions, as opposed to state changes) is better suited to the redux design.
The nice thing is you don't have to change any code outside of the saga. I've used this one for autosaving form data and it worked great.
Here's an example of setting up a saga to persist on a few redux-form actions; note that it's not limited to persisting - you can do whatever you want during a saga.
function* autosaveSaga(apiClient) {
yield throttle(500,
reduxFormActionTypes.CHANGE,
saveFormValues,
apiClient);
yield takeLatest(
reduxFormActionTypes.ARRAY_PUSH,
saveFormValues,
apiClient);
yield takeLatest(
reduxFormActionTypes.ARRAY_POP,
saveFormValues,
apiClient);
}
If you want to listen to all action types, or do custom filtering of which actions fire your persistence code, you can pass a pattern, or a matching function to takeLatest:
yield takeLatest(
'*',
saveFormValues,
apiClient);
More on what you can pass to take, takeLatest etc can be found in the docs for take.

Categories

Resources