How is "ReactReduxContext.Consumer" working? - reactjs

I wrote a demo to try to understand how "ReactReduxContext.Consumer" works, the main code like this:
Hello.tsx
export default function Hello() {
return <ReactReduxContext.Consumer>
{({store}) => {
return <div>
<ul>
<input value={store.getState().name} onChange={(e) => store.dispatch(changeNameAction(e.target.value))}/>
<div>{JSON.stringify(store.getState())}</div>
</ul>
</div>
}}
</ReactReduxContext.Consumer>
}
Entry.tsx
ReactDOM.render(
<Provider store={store}>
<Hello/>
</Provider>,
document.body
);
State.ts
export type State = {
name: string
};
reducer.ts
const initStore: State = {
name: 'aaa'
}
export default function reducers(state = initStore, action: ChangeNameAction): State {
switch (action.type) {
case 'CHANGE_NAME':
return {
...state,
name: action.name
};
default:
return state;
}
}
action.ts
export type ChangeNameAction = {
type: 'CHANGE_NAME',
name: string,
}
export function changeNameAction(name: string): ChangeNameAction {
return {
type: 'CHANGE_NAME',
name: name
}
}
It renders correctly:
But if I type anything in the text field, the value doesn't change.
I'm not sure why it doesn't work.
Here is a tiny but complete demo of this question: https://github.com/freewind-demos/typescript-react-redux-context-consumer-demo

ReactReduxContext is a React Context that contains the current Redux store.
The store reference never changes in normal operation, which means that anyone listening directly to the context in the manner you are doing will never see updates.
React Redux's connect() and useSelector both subscribe to the store and 'notify' React of the updates (usually by setting some state, which causes a re-render of the subscribed component).
You could implement your own primitive version of useSelector by doing this:
function useSelector(f) {
const store = React.useContext(ReactReduxContent);
const [state, setNextState] = React.useState(() => {
return f(store.getState());
});
React.useEffect(() => {
return store.listen(state => {
setNextState(f(store.getState()));
});
}, [f, store]);
return state;
}
React Context only propagates changes when the value passed to the provider changes, which is why you must subscribe to the store itself - as mentioned earlier, the store will never change in normal operation.

Related

Why react component can re-render when redux reducer do not return a new array?

We know that if we want to re-render a react component connected by react-redux, we should return a new array in reducer when we dispatch a related action. My problem is that why react component can re-render when redux reducer do not return a new array?
When I click App component button, first app name becomes 'aaa', why???
actions:
export function setAppName(name: string) {
return {
type: 'SET_APP_NAME',
name,
}
}
reducers:
export function appInfos(state = {
appInfos: [
{name: 'ccc'},
{name: 'ddd'},
]
}, action: any) {
switch(action.type) {
case 'SET_APP_NAME':
// just return origin array, not a new array
const appInfos = state.appInfos;
appInfos[0].name = action.name
return Object.assign({}, state, { appInfos })
default:
return state;
}
}
component App:
function mapStateToProps(state: any) {
return {
app: state.appInfos
}
}
function App() {
const { app } = useSelector(mapStateToProps)
const dispatch = useDispatch()
const onClick = useCallback(() => {
dispatch(setAppName('aaa'))
}, [dispatch])
return (
<div className="app">
{app.appInfos.map((info: any) => {
return <div key={info.name}>{info.name}</div>
})}
<button onClick={onClick}>click</button>
</div>
);
}
container & store:
const store = createStore(combineReducers({
appInfos
}))
ReactDOM.render(
<Provider store={store}>
<React.StrictMode>
<App />
</React.StrictMode>
</Provider>,
document.getElementById('root')
);
You're mixing up usage for connect and useSelector, and that's causing unnecessary renders.
connect accepts a mapStateToProps function as an argument, which must return an object because all fields will become new props to the component. That component will re-render if any individual field changes to a new reference:
https://react-redux.js.org/using-react-redux/connect-mapstate
However, useSelector accepts a selector function that must return one value, and it will re-render if that one reference changes to a new value:
https://react-redux.js.org/api/hooks#equality-comparisons-and-updates
Right now, you are writing a function that is always returning a new object reference. Your function will always run after every dispatched action, and thus it will always force the component to re-render.
Change it to just be:
function selectAppInfo(state) {
return state.appInfos
}

mobx UseObserver not re-rendering on store update

I am trying to create a simple react app Using MobX, TypeScript and React Hooks, where I will type a name of a city, and on clicking an add button, it will update the list of the cities in the UI with the newly added city.
CodeSandbox demo here
The problem is, the list is not getting updated when i click the add button with a new name of a city.
Please help
There are two problems with your code:
Don't use useLocalStore (context.tsx) for creating the store, create the store by directly calling the store function, and wrap the return value of createStore function (store.ts) in an observable
// context.tsx
// const store = useLocalStore(createStore);
const store = createStore()
// store.ts
import { observable } from "mobx";
export const createStore = () => {
const store = observable({
Cities: ["Gotham"],
get allCities(): Array<string> {
return store.Cities;
},
get cityCount(): Number {
return store.Cities.length;
},
addCity: (city: string) => {
console.log("store: adding city: " + city);
store.Cities.push(city);
console.log(store.Cities);
}
});
return store;
};
export type TStore = ReturnType<typeof createStore>;
The return value of your CityView component needs to be wrapped in the useObserver function from mobx-react-lite
// city.tsx
import { useObserver } from "mobx-react-lite";
export const CityView: React.FC<{ cities: string[] }> = ({ cities }) => {
return useObserver(() => {
return (
<ul>
{cities.map((city: string) => (
<li key={city}>{city}</li>
))}
</ul>
);
});
};
code sandbox

Is it possible to use the same <Context.Provider> on multiple components?

I know that I can wrap HOC with my <Context.Provider> and consume it in all child components.
I would like to consume context in two separate components but they are nested somewhere deeply and closest parent of them is somewhere in the app root. I don't want to provide context to (almost) all components, so I was wondering is it possible to wrap only those two components?
I tried to do it but only first component gets context.
The App structure looks like this:
<App>
<A1>
<A2>
<MyContext.Provider>
<Consumer1/>
</MyContext.Provider>
</A2>
</A1>
<B1>
<B2>
<MyContext.Provider>
<Consumer2/>
</MyContext.Provider>
</B2>
</B1>
</App>
EDIT: I was wrong thinking that wrapping root component will make re-render all child components on context change. Only consumers will rerender so it's perfectly fine to wrap root component.
If you want to have a single value which is shared between multiple parts of the application, then in some form you will need to move that value up to the common ancestor component of the ones that need to consume the value. As you mentioned in the comments, your issue is one of performance and trying not to rerender everything. Having two providers doesn't really help with this, because there will still need to be some component making sure both providers are providing the same value. So that component will end up needing to be a common ancestor of the two providers.
Instead, you can use shouldComponentUpdate (for class components) or React.memo (for functional components) to stop the rerendering process from working its way down the component tree. Deep descendants which are using Context.Consumer will still rerender, and so you can skip over the middle parts of your tree. Here's an example (note the use of React.memo on the intermediate component):
const Context = React.createContext(undefined);
const useCountRenders = (name) => {
const count = React.useRef(0);
React.useEffect(() => {
count.current++;
console.log(name, count.current);
});
}
const App = () => {
const [val, setVal] = React.useState(1);
useCountRenders('App');
React.useEffect(() => {
setTimeout(() => {
console.log('updating app');
setVal(val => val + 1)
}, 1000);
}, [])
return (
<Context.Provider value={val}>
<IntermediateComponent />
</Context.Provider>
);
}
const IntermediateComponent = React.memo((props) => {
useCountRenders('intermediate');
return (
<div>
<Consumer name="first consumer"/>
<UnrelatedComponent/>
<Consumer name="second consumer"/>
</div>
);
})
const Consumer = (props) => {
useCountRenders(props.name);
return (
<Context.Consumer>
{val => {
console.log('running consumer child', props.name);
return <div>consuming {val}</div>
}}
</Context.Consumer>
)
}
const UnrelatedComponent = (props) => {
useCountRenders('unrelated');
return props.children || null;
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.development.js"></script>
<div id="root"></div>
When you run the above code, check the logs to see which components rerender. On the first pass, everything renders, but then after a second when the app state changes, only app rerenders. IntermediateComponent, UnrelatedComponent, and even Consumer don't rerender. The function inside the Context.Consumer does rerun, and any thing returned by that function (in this case just a div) will rerender.
As requested by the OP this solution uses mostly hooks but useReducer cannot achieve state sharing under separate providers (as far as I've tried).
It does not require one Provider to be at the root of the app and different reducer can be used for each Provider.
It uses a static state manager but that's an implementation detail, to share the state between several component under different context proviver, one will need something like a shared reference to the state, a way to change it and a way to notify of these changes.
When using the snippet the first button shows the shared state and increment foo when clicked, the second button shows the same shared state and increments bar when clicked:
// The context
const MyContext = React.createContext();
// the shared static state
class ProviderState {
static items = [];
static register(item) {
ProviderState.items.push(item);
}
static unregister(item) {
const idx = ProviderState.items.indexOf(item);
if (idx !== -1) {
ProviderState.items.splice(idx, 1);
}
}
static notify(newState) {
ProviderState.state = newState;
ProviderState.items.forEach(item => item.setState(newState));
}
static state = { foo: 0, bar: 0 };
}
// the state provider which registers to (listens to) the shared state
const Provider = ({ reducer, children }) => {
const [state, setState] = React.useState(ProviderState.state);
React.useEffect(
() => {
const entry = { reducer, setState };
ProviderState.register(entry);
return () => {
ProviderState.unregister(entry);
};
},
[]
);
return (
<MyContext.Provider
value={{
state,
dispatch: action => {
const newState = reducer(ProviderState.state, action);
if (newState !== ProviderState.state) {
ProviderState.notify(newState);
}
}
}}
>
{children}
</MyContext.Provider>
);
}
// several consumers
const Consumer1 = () => {
const { state, dispatch } = React.useContext(MyContext);
// console.log('render1');
return <button onClick={() => dispatch({ type: 'inc_foo' })}>foo {state.foo} bar {state.bar}!</button>;
};
const Consumer2 = () => {
const { state, dispatch } = React.useContext(MyContext);
// console.log('render2');
return <button onClick={() => dispatch({ type: 'inc_bar' })}>bar {state.bar} foo {state.foo}!</button>;
};
const reducer = (state, action) => {
console.log('reducing action:', action);
switch(action.type) {
case 'inc_foo':
return {
...state,
foo: state.foo + 1,
};
case 'inc_bar':
return {
...state,
bar: state.bar + 1,
};
default:
return state;
}
}
// here the providers are used on the same level but any depth would work
class App extends React.Component {
render() {
console.log('render app');
return (
<div>
<Provider reducer={reducer}>
<Consumer1 />
</Provider>
<Provider reducer={reducer}>
<Consumer2 />
</Provider>
<h2>I&apos;m not rerendering my children</h2>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
In the end we have re created something a little like redux with a static state shared by several Provider/Consumer ensembles.
It will work the same no matter where and how deep the providers and consumers are without relying on long nested update chains.
Note that as the OP requested, here there is no need for a provider at the root of the app and therefore the context is not provided to every component, just the subset you choose.

Redux, React, MapStateToProps: Retrieve a specific element in an array from the redux state tree

I have a reducer named "leagues" in the redux state tree, which is just an array of individual league objects. Each league has a unique id (assigned in the backend), and a name.
I'm trying to write a Component that represents an individual league, and so I want to have a mapStateToProps function that retrieves the correct league. The correct league is known from the url, that is, through the match prop in react-router-v4.
I tried:
function mapStateToProps(state, ownProps) {
return {
league: state.leagues.find(aLeague => aLeague.id === ownProps.match.params.id)
}
}
But that led to an error "state.leagues.find" is not a function.
Then I tried
function mapStateToProps(state, ownProps) {
return {
league: state.leagues[ownProps.match.params.id]
}
}
which doesn't error, but retrieves the wrong league - if the id is 3, then this retrieves state.leagues[3], instead of state.leagues.where(league.id === 3)
The value of leagues when I attempt to access the a single league's page:
leagues: [
{
id: 54,
name: 'Test League'
},
{
id: 55,
name: 'Another Test'
}
],
And the leagues reducer code:
const initialState = {
leagues: []
};
export default (state = initialState, action = {}) => {
switch(action.type) {
case SET_USER_LEAGUES:
return state = action.leagues
case ADD_USER_LEAGUE:
return [
...state,
{
id: action.league.id,
name: action.league.name,
}
];
default: return state;
}
}
Any help is much appreciated.
Thanks!
This is because when the redux store is initialized you most likely are setting the initial state to null which is why you get an error state.leagues.find is not a function and once the state is resolved through an async ajax call then the state is there. I recommend to make that kind of logic in a react lifecycle method like componentDidMount and set the state there for the league once the leagues state is available. like this:
componentDidMount() {
const { leagues } = this.state;
if (leagues && leagues.length > 0) {
// update the state to the correct league here and this.props.id is the id that you want
const league = leagues.find(aLeague => aLeague.id === this.props.id);
this.setState({
league
}
}
}
I hope that helps.
It seems like when your component first renders, its default state has been set to null and causes the app to crash when you try to use array method find on a null object. Set your initial state for your leagues reducer to an empty array.
Then if your array is empty, your app probably hasn't retrieved results yet, and you can display a message like "Loading...".
However, this doesn't solve the problem of you actually have 0 items in your database, for example. Then you'll show falsely show loading even when there is 0 records.
For that, I would also suggest adding a isLoading reducer (with default state true), that maintains the state of your application during the time it is fetching async data. When your async calls complete, dispatch an action to update the appstate and set isLoading to false. Only then should you try to retrieve values from your leagues reducer.
I would also suggest you have another "league" reducer that does the filtering so you don't have to maintain this logic in your component.
I see that Array.prototype.find is not supported in IE. So, there could be a browser compatibility issue.
You can always use Array.prototype.filter instead (assuming that state.leagues will always be an Array:
function mapStateToProps(state, ownProps) {
const { leagues = [] } = state;
return {
league: leagues.filter(aLeague => aLeague.id === ownProps.match.params.id)[0]
}
}
Your reducer looks wrong to me. Could you try this:
export default (state = initialState, action = {}) => {
switch (action.type) {
case SET_USER_LEAGUES:
return {
...state,
leagues: action.leagues,
};
case ADD_USER_LEAGUE:
const leagues = [...state.leagues, { id: action.league.id, name: action.league.name, }];
return {
...state,
leagues,
};
default:
return state;
}
}
Some of your functions are manipulating the state and changing between returning a bool, an object and an array.
To resolve this, I now use an object instead of an array. (See reference 1 below for the reason). And I render the component after the store state is loaded so I can use mapStateToProps. I've put some code extracts below from working code. Hopefully also provides some tips on how to use Redux reducers for this use case. Please excuse any typos, I edited the code extracts inline here.
Store
import { createStore, applyMiddleware, combineReducers } from 'redux'
import thunkMiddleware from 'redux-thunk'
import * as reducers from './reducers'
const initialState = {
items: {
/* id(1): {id: PropTypes.number.isRequired, name: PropTypes.string}
id(n): {id: PropTypes.number.isRequired, name: PropTypes.string} */
}
}
var rootReducer = combineReducers(reducers)
window.store = createStore(rootReducer, initialState, applyMiddleware(thunkMiddleware))
Action
export const UPDATE_ITEM = 'UPDATE_ITEM'
export const updateItem = item => ({
type: UPDATE_ITEM,
item
})
Reducer
import { UPDATE_ITEM } from './actions'
export const items = (state = null, action) => {
switch (action.type) {
case UPDATE_ITEM:
let id = action.item.id
return Object.assign({}, state, {
[id]: Object.assign({}, state[id], action.item)
})
default:
return state
}
}
Add some objects to the store
import { updateItem } from './actions'
var item1 = {id: 1, name: 'Alice'}
window.store.dispatch(updateItem(item1))
var item2 = {id: 2, name: 'Bob'}
window.store.dispatch(updateItem(item2))
SomeComponent mapStateToProps
function mapStateToProps (state, ownProps) {
return {
item: state.items[ownProps.id]
}
}
Load Component like this after the store is populated.
ReactDOM.render(
<Provider store={window.store}>
<SomeComponent id={1} />
<SomeComponent id={2} />
</Provider>,
document.getElementById('root')
)
Just wanted to note that my main motivation to solve this was that I was mapping the entire object state (state.items) to props, but then render was called after an update to any array element which was horrible for performance.
References:
[1] https://stackoverflow.com/a/36031765/1550587

setState works but redux store update doesn't

First, I want to mention that the only thing I'm changing between two approaches is setState vs going through the Redux store. Not changing anything else i.e. components, etc.
If I use the setState approach, I can close my modal but if I go through the store, it doesn't close. Any idea why?
Here's my reducer:
import 'babel-polyfill';
import * as types from '../actions/actionTypes';
const initialState = {
modals: {
"modal1": { isDisplayed: true },
"modal2": { isDisplayed: false }
}
};
export default (state = initialState, action) => {
switch (action.type) {
case types.SET_IS_DISPLAYED_MODAL :
return Object.assign({}, state, {
modals: action.modals
})
default: return state
}
}
}
Here are the two versions of my onClick action that is supposed to close the modal.
This is the setState version and it works:
displayModal(modalId, value)
{
let modals = this.props.modals;
modals[modalId].isDisplayed = value;
return setState({modals: modals});
}
And here's the version that goes through the redux store and it does NOT close my modal.
displayModal(modalId, value)
{
let modals = this.props.modals;
modals[modalId].isDisplayed = value;
return this.props.actions.displayModal(modals);
}
There's not much to the action but here it is:
export const displayModal = (modals) => {
return {
type: types.SET_IS_DISPLAYED_MODAL,
modals
};
}
Just so you see how it looks in my component, here it is:
render() {
return(
<div>
<div>Some info...</div>
{this.props.modals["modal1"].isDisplayed
? <Modal1 />
: null}
{this.props.modals["modal2"].isDisplayed
? <Modal2 />
: null}
</div>
);
}
BTW, I know that I'm hitting the action and the reducer. I also know that if I put a debugger in my mapStateToProps, I'm hitting it with updated state for my modals. So I know both the action and the reducer are doing what they're supposed to.
UPDATE:
I just tried something and this fixed the issue. I added last line to mapStateToProps and updated the section in my component:
function mapStateToProps(state) {
return {
modals: state.modals,
isModal1Displayed: state.modals["modal1"].isDisplayed // Just added this line
}
}
And changed the code in my component to:
render() {
return(
<div>
<div>Some info...</div>
{this.props.isModal1Displayed
? <Modal1 />
: null}
</div>
);
}
First of all, never mutate state in Redux reducer - it must be a pure function to work and detect changes correctly. Same rules apply to objects which you get with props.
You must change your code so you only dispatch an action to the store and reduce it to a new state.
First, dispatch an action:
displayModal(modalId, value)
{
this.props.actions.displayModal(modalId, value);
}
Your action will carry information which modal to hide or show:
export const displayModal = (modalId, value) => {
return {
type: types.SET_IS_DISPLAYED_MODAL,
modalId,
value
};
}
Then you can reduce it:
export default (state = initialState, action) => {
switch (action.type) {
case types.SET_IS_DISPLAYED_MODAL :
return Object.assign({}, state,
{
modals: Object.assign({}, state.modals,
{
[action.modalId]: { isDisplayed: action.value }
})
})
default: return state
}
}
As you can see there is a lot of boilerplate here now. With ES6 and ES7 you can rewrite your reducer with the object spread operator or you can use Immutable.js library, which will help you with setting properties deep in the hierarchy.
Reducer with object spread operator looks like this:
export default (state = initialState, action) => {
switch (action.type) {
case types.SET_IS_DISPLAYED_MODAL :
return {
...state,
modals: {
...state.modals,
[action.modalId]: { isDisplayed: action.value }
}
}
default: return state
}
}
You may ask yourself why your fix works. Let me explain.
You change a modal state when you dispatch an action to the Redux by mutating state in place modals[modalId].isDisplayed = value. After that the action is dispatched, reduced and mapToProps gets called again. There is probably reference check in connect higher order component and you have mutated the modal but not the modals object so it has the same reference = your component doesn't re-render. By adding a isModal1Displayed field you are actually disabling optimizations because there is boolean comparison, not a reference check and your component rerenders.
I hope it will help you with understanding Redux and it's principles.

Resources