I have a redux (sub) state that consists of a large number of similar entries.
export type PartnerCalculatorStateShape = {
m16_19:number;
m20_24:number;
m25_34:number;
m35_44:number;
m45_64:number;
m65_plus:number;
f16_19:number;
f20_24:number;
f25_34:number;
f35_44:number;
f45_64:number;
f65_plus:number;
};
I am using the Redux Toolkit so my reducer is of this form (note that Redux Toolkit uses immutable update logic, so I can assign modify the state directly)
type PartnerCalculatorPayload = {
key:string;
value:number;
}
export const partnerCalculatorSlice = createSlice({
name: 'PartnerCalculator',
initialState,
reducers: {
partnerCalculatorValueReceived(state, action: PayloadAction<PartnerCalculatorPayload>) {
state[action.payload.key] = action.payload.value;
}
}
});
I'm a bit stuck on how to use useSelector. What I want to do is have a selector function in my Redux file, something like this
export const selectorFunction = (state,key) => state[key]
where key would be m20_24, for example. Then I would use that selector function in my React component
const myVar = useSelector(selectorFunction)
But how do I pass in the key?
The official hooks documentation recommends using closure to pass additional variables
import React from 'react'
import { useSelector } from 'react-redux'
export const TodoListItem = props => {
const todo = useSelector(state => state.todos[props.id])
return <div>{todo.text}</div>
}
However. useSelector will be in my React component and I want to keep the selector function I pass to useSelector inside my redux file, so I can't see how to use closure.
I suppose I could just pass the entire state out from my selector function
const selectorFunction = state => state
and then treat it as an object in my React component and key into it there
const myState = useSelector(selectorFunction)
const myVar = myState["m20_24"]
but that seems kind of ugly.
If that's the way to go, would myVar update anytime any of the fields in my Redux state changed? I'm a bit unclear as to how the useSelector equality testing mechanism works -- it says it uses 'strict equality', so if any part of my state object changed (that is, if the field 'm20_24' changed) then myVar would be updated?
Thanks for any insights!
Pass an anonymous selector to useSelector, and then call the actual selector inside of there:
const value = useSelector(state => selectValueByKey(state, props.key));
Related
I have seen many example codes of redux and found that we use useSelector in mostly 2 ways.
nested
import React from 'react'
import { useSelector } from 'react-redux'
export const UserComponent = () => {
const name = useSelector((state) => state.user.name)
const age = useSelector((state) => state.user.age)
return <div>{name}, {age}</div>
}
non-nested
import React from 'react'
import { useSelector } from 'react-redux'
export const UserComponent = () => {
const user = useSelector((state) => state.user)
return <div>{user.name}, {user.age}</div>
}
As you can see, with approach 2, I will end up saving lots of code repetition and I only need to create 1 selector.
So I wanted to know the difference, any performance benefits, etc.
There is a difference between these two approaches, let's dive deeper into the useSelector hook and see how it works. So let's say you're the creator of this hook and you define it as:
// this is your redux state
const state = { user: { name: 'Haseeb', age: 10 } };
// defining our own selector hook
const useSelector = (selectorFunction) => {
const accessedProperty = selectorFunction(state);
// accessedProperty will contain whatever is returned from the selectorFunction
return accessedProperty;
}
Now, let's use our hook
// nested, subscribe to just the name property in user
const name = useSelector((state) => state.user.name);
// non-nested, subscribe to the complete user object
const user = useSelector((state) => state.user);
Talking about the performance, When an action is dispatched, useSelector() will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render.
So in your nested approach, useSelector will only do a reference equality comparison of the name value. AND in your non-nested approach, useSelector will do a reference equality comparison of the complete user object. In both cases, the result of the comparison determines whether the component should re-render. Chances are the non-nested approach will cause more re-renders than the nested approach because the non-nested approach is subscribing to more values than the nested approach.
Call useSelector() multiple times, with each call returning a single field value but it is also okay to use one useSelector() if further performance optimizations are not necessary
I have a custom hook: useNavigation(). It returns the target route to redirect the user if it is required.
Inside the hook I use redux store (only to read values):
{ prop1, prop2 } = useTypedSelector(state => state.data);
Where useTypedSelector code is:
const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
Where TypedUseSelectorHook and useSelector hooks come from react-redux and RootState is a type of my root reducer.
I have the following actual result:
If redux store changes, then the change triggers unexpected useNavigation hook call.
But my expected result is:
useNavigation hook uses redux store to read values, but they don't triggers useNavigation hook call on change.
How to prevent extra hook calls on redux state change?
You always want to select the minimum amount of data that you need with useSelector. Any changes to the value that is returned from useSelector will trigger a re-render.
You can make it marginally better by selecting prop1 and prop2 separately so that changes to other properties in state.data don't trigger re-rendering.
const prop1 = useTypedSelector(state => state.data.prop1);
const prop2 = useTypedSelector(state => state.data.prop2);
But I suspect that you can make it much better by moving some of the logic that is happening in your hook into a custom selector function instead. You may want to use reselect to memoize your selector.
const selectTargetRoute = (state: RootState) => {
const {prop1, prop2} = state.data;
// do some logic
// return a route or null/undefined
}
const useNavigation = () => {
const targetRoute = useTypedSelector(selectTargetRoute);
// do something with the route
}
Currently i implemented React Hooks with Redux. Now the only issue is that when changing the Redux state the performance is going down in my app e.g:
import { cardItemIsDragging, storeWidgetDrag } from '../../redux/actions';
then:
/** pass currentUser and currentChannel from redux to global props */
const mapStateToProps = state => ({
storeWidget: state.storewidgetinfo.storeWidgetInfo,
storeDrag: state.storewidgetdrag.storeWidgetDrag,
storeDelete: state.storewidgetdelete.storeWidgetDelete,
});
and also having:
/** #component */
export default connect(mapStateToProps, {
cardItemIsDragging,
storeWidgetDrag,
})(Layout);
Now inside of this component there is a function that will update the Redux state:
/** Calls when drag starts. */
const onDragStart = () => {
const { cardItemIsDragging } = props;
cardItemIsDragging(true);
};
Im updating this state because im also using it in another component.
The code above is resulting in a performance drop of the component/app, so my question is what is the best practice to setup Redux without losing performance / unnecessary re-rendering of the component, but keep changing state while it is needed in another component.
I have some value that comes from Redux through props and I want to NOT render the component again when this value changes.
I found some answers saying that I can use Memo but I don't know if this is the best option for my case?
My "code":
const MyComponent = () => {
return ...;
}
const mapStateToProps = state => ({
myVar: state.myVar
});
export default connect(mapStateToProps)(MyComponent);
myVar changing shouldn't re render the component in this case.
React.memo can do the job, you can pass a custom equality check function to perform a rerender only when it returns a falsy value. I never faced a case where you want to completely ignore a value update from your Redux store, maybe it shouldn't be stored there ?
Memo API
eg: React.memo(Component, [areEqual(prevProps, nextProps)])
UseSelector API
Another way would be to use useSelector with a custom equality check function:
useSelector Redux API Reference
Connect API
If you still want to stick with mapStateToProps, you can also pass a custom equality check function as a parameter of the connect function:
areStatePropsEqual Redux API Reference
Edit: useRef solution
By using useRef, you store a mutable variable that will be kept as it is for the whole lifetime of the component.
Example based on yours:
const StoreMyVar = (WrappedComponent) => ({myVar, ...props}) => {
const myRefVar = useRef(myVar)
return <WrappedComponent myVar={myRefVar} {...props} />
}
const MyComponentWithImmutableVar = StoreMyVar(MyComponent)
The fastest way is React.memo, but you can use it just with functional components. Be careful, it is not tested.
const MyComponent(props) {
return ...;
}
const areEqual(prevProps, nextProps) {
return prevProps.myVar === nextProps.myvar
}
const mapStateToProps = state => ({
myVar: state.myVar
});
export default React.memo(MyComponent, areEqual);
I am trying to find a good, clean, with little boilerplate, way to handle React's global state
The idea here is to have a HOC, taking advantage of React's new Hooks & Context APIs, that returns a Context provider with the value bound to its state. I use rxjs for triggering a state update on store change.
I also export a few more objects from my store (notably : the raw rxjs subject object and a Proxy of the store that always returns the latest value).
This works. When I change something in my global store, I get updates anywhere in the app (be it a React component, or outside React). However, to achieve this, the HOC component re-renders.
Is this a no-op ?
The piece of code / logic I think could be problematic is the HOC component:
const Provider = ({ children }) => {
const [store, setStore] = useState(GlobalStore.value)
useEffect(() => {
GlobalStore.subscribe(setStore)
}, [])
return <Context.Provider value={store}>{children}</Context.Provider>
}
GlobalStore is a rxjs BehaviorSubject. Every time the subject is updated, the state of the Provider component gets updated which triggers a re-render.
Full demo is available there: https://codesandbox.io/s/qzkqrm698q
The real question is: isn't that a poor way of doing global state management ? I feel it might be because I basically re-render everything on state update...
EDIT: I think I have written a more performant version that's not as lightweight (depends on MobX), but I think it generates a lot less overhead (demo at: https://codesandbox.io/s/7oxko37rq) - Now what would be cool would be to have the same end result, but dropping MobX - The question makes no sense anymore
I understand your need to handle a global state. I already found myself in the same situation. We have adopted similar solutions, but in my case, I've decided to completelly drop from ContextAPI.
The ContextAPI really sucks to me. It seems to pretend to be a controller based pattern, but you end up wrapping the code inside an non-sense HOC. Maybe I've missed he point here, but in my opinion the ContextAPI is just a complicated way to offer scoped based data flow.
So, I decided to implement my own global state manager, using React Hooks and RxJS. Mainly because I do not use to work on really huge projects (where Redux would fit perfectly).
My solution is very simple. So lets read some codes because they say more than words:
1. Store
I've created an class only to dar nome aos bois (it's a popular brazilian expression, google it 😊) and to have a easy way to use partial update on BehaviorSubject value:
import { BehaviorSubject } from "rxjs";
export default class Store<T extends Object> extends BehaviorSubject<T> {
update(value: Partial<T>) {
this.next({ ...this.value, ...value });
}
}
2. createSharedStore
An function to instantiate the Store class (yes it is just because I don't like to type new ¯\(ツ)/¯):
import Store from "./store";
export default function <T>(initialValue: T) {
return new Store<T>(initialValue);
}
3. useSharedStore
I created an hook to easily use an local state connected with the Store:
import Store from "./store";
import { useCallback, useEffect, useState } from "react";
import { skip } from "rxjs/operators";
import createSharedStore from "./createSharedStore";
const globalStore = createSharedStore<any>({});
type SetPartialSharedStateAction<S> = (state: S) => S;
type SetSharedStateAction<S> = (
state: S | SetPartialSharedStateAction<S>
) => void;
export default function <T>(
store: Store<T> = globalStore
): [T, SetSharedStateAction<T>] {
const [state, setState] = useState(store.value);
useEffect(() => {
const subscription = store
.pipe(skip(1))
.subscribe((data) => setState(data));
return () => subscription.unsubscribe();
});
const setStateProxy = useCallback(
(state: T | SetPartialSharedStateAction<T>) => {
if (typeof state === "function") {
const partialUpdate: any = state;
store.next(partialUpdate(store.value));
} else {
store.next(state);
}
},
[store]
);
return [state, setStateProxy];
}
4. ExampleStore
Then I export individual stores for each feature that needs shared state:
import { createSharedStore } from "hooks/SharedState";
export default createSharedStore<Models.Example | undefined>(undefined);
5. ExampleComponent
Finally, this is how to use in the component (just like a regular React state):
import React from "react";
import { useSharedState } from "hooks/SharedState";
import ExampleStore from "stores/ExampleStore";
export default function () {
// ...
const [state, setState] = useSharedState(ExampleStore);
// ...
function handleChanges(event) {
setState(event.currentTarget.value);
}
return (
<>
<h1>{state.foo}</h1>
<input onChange={handleChange} />
</>
);
}
GlobalStore subject is redundant. RxJS observables and React context API both implement pub-sub pattern, there are no benefits in using them together this way. If GlobalStore.subscribe is supposed to be used in children to update the state, this will result in unnecessary tight coupling.
Updating glubal state with new object will result in re-rendering the entire component hierarchy. A common way to avoid performance issues in children is to pick necessary state parts and make them pure components to prevent unnecessary updates:
<Context.Consumer>
({ foo: { bar }, setState }) => <PureFoo bar={bar} setState={setState}/>
</Context.Provider>
PureFoo won't be re-rendered on state updates as long as bar and setState are the same.