Using shouldComponentUpdate for block component re-render every second - reactjs

That game developed with react and redux.I am not react-redux developer(I am .net developer) but I must continue that project so I am new in react and redux
That game performance is too bad in some android phones.So I analyze project.I see that components render method works every second.My component contain more than 30 other components.So every secon it re render and this is cause bad performance in some old android phones
Why React component re render every second?Can I block this?
I search for that problem I see that solution is shouldComponentUpdate function
shouldComponentUpdate(nextProps,nextState) {
console.log(nextProps.gameStore.get('state'));//waiting
console.log(this.props.gameStore.get('state'));//waiting
console.log(this.state);
console.log(nextState);
if (nextProps.gameStore.get('state')==this.props.gameStore.get('state')) {
return false;
}
else {
return true;
}
}
but in this function nextstate and this state is same,
nextProps.gameStore.get('state') and this.props.gameStore.get('state') is the same.Why next state and current state is same?What should I do?I use constructor but it is still same here is all my component code
import React from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
//import { default as HTML5Backend } from 'react-dnd-touch-backend';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { bindFirstArguments } from 'utils/bindFirstArgument';
import * as OkeyGameActions from 'actions/OkeyGameActions';
import * as OkeyNetActions from 'actions/OkeyNetActions';
import * as OkeyChatActions from 'actions/Chatactions';
import { SeatDirection } from 'constants/AppConstants';
import { OkeyScoreboardDialog }
from 'components/OkeyScoreboardDialog/OkeyScoreboardDialog';
import OkeyMatchResultDialog
from 'components/OkeyMatchResultDialog/OkeyMatchResultDialog';
import OkeyRackWrapper from 'components/OkeyRackWrapper/OkeyRackWrapper';
import OkeyTopToolbar from 'components/OkeyTopToolbar/OkeyTopToolbar';
import OkeyTableToolbar from 'components/OkeyTableToolbar/OkeyTableToolbar';
import OkeyTableCenter from 'components/OkeyTableCenter/OkeyTableCenter';
import CustomDragLayer from 'components/CustomDragLayer/CustomDragLayer';
import MessageList from 'components/chat/MessageList';
import PrivateEastMessageList from 'components/chat/PrivateEastMessageList';
import PrivateNorthMessageList from 'components/chat/PrivateNorthMessageList';
import PrivateWestMessageList from 'components/chat/PrivateWestMessageList';
import PrivateSouthMessageList from 'components/chat/PrivateSouthMessageList';
import './_OkeyGame.scss';
function toJS(item) {
if (item === null) {
return null;
}
//var item1=item.toJS();
//if (item1.color==='BLACK') {
// var a='a';
//}
if (item == undefined) {
return;
}
return item.toJS();
}
function getRelativeDirection(selfSeat, direction) {
let relativeDirection = direction;
if (selfSeat >= 0) {
relativeDirection = (selfSeat - direction + 4) % 4;
}
return relativeDirection;
}
class OkeyGame extends React.Component {
constructor(props) {
super(props);
}
shouldComponentUpdate(nextProps,nextState) {
console.log(nextProps.gameStore.get('state'));//waiting
console.log(this.props.gameStore.get('state'));//waiting
console.log(this.state);
console.log(nextState);
if (nextProps.gameStore.get('state')==this.props.gameStore.get('state')) {
return false;
}
else {
return true;
}
}
render() {
const { dispatch, gameStore, gamePlay, playRules } = this.props;
let actions = bindActionCreators(OkeyGameActions, dispatch);
let netActions = bindActionCreators(OkeyNetActions, dispatch);
const currentTurn = gameStore.get('currentTurn');
const playState = {
selectedStone: gamePlay.get('selectedStone'),
gosterge: gamePlay.get('gosterge'),
middleStoneCount: gamePlay.get('middleStoneCount'),
currentTurn: currentTurn
};
if (playState.gosterge != undefined) {
window.localStorage.setItem('gostergeNumber', playState.gosterge._root.entries[0][1]);
window.localStorage.setItem('gostergeColor', playState.gosterge._root.entries[1][1]);
}
const hasOpenedStonesThisTurn = {
hasOpenedSequenceThisTurn: playRules.get('hasOpenedSequenceThisTurn'),
hasOpenedPairsThisTurn: playRules.get('hasOpenedPairsThisTurn')
};
const rules = {
canOpenSequence: playRules.get('canOpenSequence'),
canOpenPairs: playRules.get('canOpenPairs'),
canWithdraw: playRules.get('canWithdraw'),
canDiscard: playRules.get('canDiscard'),
canCollectOpen: playRules.get('canCollectOpen'),
canLeaveTaken: playRules.get('canLeaveTaken'),
canProcessStone: playRules.get('canProcessStone')
};
const discardMiniBoxes = {
discardMiniBoxPairs: gamePlay.get('pairs'),
discardMiniBoxSequence: gamePlay.get('sequence')
};
const selfSeat = gameStore.get('selfSeat');
const { westSeat, eastSeat, northSeat, southSeat } =
{
westSeat: getRelativeDirection(selfSeat, SeatDirection.WEST),
eastSeat: getRelativeDirection(selfSeat, SeatDirection.EAST),
northSeat: getRelativeDirection(selfSeat, SeatDirection.NORTH),
southSeat: getRelativeDirection(selfSeat, SeatDirection.SOUTH)
};
const players = {
selfSeat: selfSeat,
pSouth: {
seatId: southSeat,
discardStones: gamePlay.getIn(['discardStones', southSeat]),
profile: toJS(gameStore.getIn(['players', southSeat])),
dispatch: dispatch
},
pNorth: {
seatId: northSeat,
discardStones: gamePlay.getIn(['discardStones', northSeat]),
profile: toJS(gameStore.getIn(['players', northSeat])),
dispatch: dispatch
},
pEast: {
seatId: eastSeat,
discardStones: gamePlay.getIn(['discardStones', eastSeat]),
profile: toJS(gameStore.getIn(['players', eastSeat])),
dispatch: dispatch
},
pWest: {
seatId: westSeat,
discardStones: gamePlay.getIn(['discardStones', westSeat]),
profile: toJS(gameStore.getIn(['players', westSeat])),
dispatch: dispatch
}
};
let profiles = [
players.pSouth.profile,
players.pEast.profile,
players.pNorth.profile,
players.pWest.profile
];
localStorage.setItem("selfSeat", selfSeat);
localStorage.setItem("roomID", gameStore.get('id'));
if (selfSeat == 0)
profiles = [players.pSouth.profile,players.pEast.profile,players.pNorth.profile,players.pWest.profile];
else if (selfSeat == 1)
profiles = [players.pWest.profile,players.pSouth.profile,players.pEast.profile,players.pNorth.profile];
else if (selfSeat == 2)
profiles = [players.pNorth.profile,players.pWest.profile,players.pSouth.profile,players.pEast.profile];
else if (selfSeat == 3)
profiles = [players.pEast.profile,players.pNorth.profile,players.pWest.profile,players.pSouth.profile];
const matchState = {
name: gameStore.getIn(['options', 'name']),
maxRounds: gameStore.getIn(['options', 'rounds']),
stake: gameStore.getIn(['options', 'stakes']),
round: gameStore.get('round')
};
const owner = gamePlay.get('ownerID');
const scoreboard = gameStore.get('scoreboard');
const matchResult = gameStore.get('matchResult');
const restCountdown = gameStore.get('restCountdown');
const roomState = gameStore.get('roomState');
const { messageList } = this.props;
const { privateEastMessageList } = this.props;
const { privateNorthMessageList } = this.props;
const { privateWestMessageList } = this.props;
const { privateSouthMessageList } = this.props;
let chatActions = bindActionCreators(OkeyChatActions, dispatch);
// const dispatch1 = this.props
// each action has a first argument of room id
netActions = bindFirstArguments(netActions, gameStore.get('id'));
let from = gameStore.get('from');
let to = gameStore.get('to');
let gift = gameStore.get('gift');
let from1 = gameStore.get('from1');
let to1 = gameStore.get('to1');
let gift1 = gameStore.get('gift1');
let from2 = gameStore.get('from2');
let to2 = gameStore.get('to2');
let gift2 = gameStore.get('gift2');
let from3 = gameStore.get('from3');
let to3 = gameStore.get('to3');
let gift3 = gameStore.get('gift3');
let arayan = gameStore.get('arayan');
let aranan = gameStore.get('aranan');
return (
<div className="game-background" style={{background: 'url(http://okey101.xyz/staticImg/background.png)',backgroundSize:'cover'}}>
<div className="okey-game flex-centered-column">
<CustomDragLayer isMini={gamePlay.get('isOver') > 0}></CustomDragLayer>
<MessageList {...chatActions} {...netActions} messageList={messageList} />
<OkeyScoreboardDialog profiles={profiles}
scoreboard={scoreboard} />
<OkeyMatchResultDialog matchResult={matchResult}
{...netActions}
{...actions}
roomState={roomState}/>
<OkeyTopToolbar {...netActions}
{...matchState}
profiles={profiles}/>
<OkeyTableCenter {...actions}
{...netActions}
{...playState}
{...rules}
{...discardMiniBoxes}
{...players}
owner={owner}
messageList={messageList}
privateEastMessageList={privateEastMessageList}
privateNorthMessageList={privateNorthMessageList}
privateWestMessageList={privateWestMessageList}
privateSouthMessageList={privateSouthMessageList}
from={from}
to={to}
gift={gift}
from1={from1}
to1={to1}
gift1={gift1}
from2={from2}
to2={to2}
gift2={gift2}
from3={from3}
to3={to3}
gift3={gift3}
arayan={arayan}
aranan={aranan}
stones={gamePlay.get('stones')}/>
<OkeyRackWrapper {...actions}
{...netActions}
{...playState}
stones={gamePlay.get('stones')}
stoneGroups={gamePlay.get('stoneGroups')}/>
<OkeyTableToolbar {...actions}
{...netActions}
{...rules}
restCountdown={restCountdown}
currentTurn={currentTurn}
{...hasOpenedStonesThisTurn}
roomState={roomState}
stones={gamePlay.get('stones')}
{...discardMiniBoxes}
okeyStone={gamePlay.get('okeyStone')}/>
</div>
</div>
);
}
}
const mapStateToProps = (state => ({
gameStore: state.gameStore,
gamePlay: state.gamePlay,
playRules: state.playRules,
messageList: state.MessageList,
privateEastMessageList: state.PrivateEastMessageList,
privateNorthMessageList: state.PrivateNorthMessageList,
privateWestMessageList: state.PrivateWestMessageList,
privateSouthMessageList: state.PrivateSouthMessageList
}));
const OkeyGameWithDnD = DragDropContext(HTML5Backend)(OkeyGame);
export default connect(mapStateToProps)(OkeyGameWithDnD);
Edit:With Aftab Khan directives I change component to PureComponent but the page does not open and there is not error in console
I change this to
const mapStateToProps = (state => ({
gameStore: toJS(state.gameStore),
gamePlay: toJS(state.gamePlay),
playRules: toJS(state.playRules),
messageList: toJS(state.MessageList),
privateEastMessageList: toJS(state.PrivateEastMessageList),
privateNorthMessageList: toJS(state.PrivateNorthMessageList),
privateWestMessageList: toJS(state.PrivateWestMessageList),
privateSouthMessageList: toJS(state.PrivateSouthMessageList)
}));
but it still does not work
then I change it to this
const mapStateToProps = (state => ({
gameStore: state.gameStore.toJS(),
gamePlay: state.gamePlay.toJS(),
playRules: state.playRules.toJS(),
messageList: state.MessageList.toJS(),
privateEastMessageList: state.PrivateEastMessageList.toJS(),
privateNorthMessageList: state.PrivateNorthMessageList.toJS(),
privateWestMessageList: state.PrivateWestMessageList.toJS(),
privateSouthMessageList: state.PrivateSouthMessageList.toJS()
}));
but it still does not open in browser

There are few things I noticed about your component
render method does all lot of initialization for the children components
shouldComponentUpdate is unnecessary as it deals only with props (which is expected as all your state is in the redux store).
PureComponent is unnecessary to given that you are connecting it to the store using connect(). Your component has not state, its better being and functional components created using a simple es6 arrow function
For 2. and 3. refer the note on optimizations connect() does for you as it creates containers.
Now its impossible to tell you why your components are being rendered every second without inspecting every line of your components. However, with carefully consideration of the following can help you make sure re-renders are triggered frugally and only when necessary.
1. Keep render() method as light-weight as possible
render is probably the most called method in all components. More bloated it is slow will you render cycles be. You can move all you localStorage.setItems to componentWillReceiveProps. Each one of those synchronous LocalStorage calls is taking up render time.
2. Remove shouldComponentUpdate. Instead pass only those props to components that it absolutely requires. Make as many components stateless pure functions
shouldComponentUpdate is an escape hatch for components that take in more props they need to render. Downside of sCU: it runs before every component re-render. In some cases it can slow down instead of speeding things up. See this comment here by jimfb. This supports my suggestion of turning as many components into stateless function components as you can - including maybe the component you posted here
3. PureComponent will not be needed if your components are stateless javascript functions taking in only the props they need.

Seems like you need React.PureComponent.
Here is the link to it. Link
Basically what it does is before rendering each component, it will check if the component has any changes in the props or the state. If there is no change it won't reRender.
Looks like you are using immutable data structures in your reducer. In ur mapStateToProps, change all the immutable data to normal JS objects calling toJS() method. Then PureComponent will work as intended.

Related

Accessing state or props from functional component

i'm trying to accessing variable/state in functional component from other component. i found out i can save the in some global state like redux or local storage. but i don't want to use any of them. i also found out about lifting state up but can't understand about it.
below is an example for my case. let's say i want to generate a lucky number from functional component GenerateLuckyNumber. and i call the component from showLuckyNumber. but i need to save all the list of lucky number that have been generated. i create a hook state to store all lucky number. but i dont know how to retrieve them from GenereateLuckyNumber
any idea how to to it?
import React from 'react';
import { Text, TouchableOpacity } from 'react-native';
import { useState, useEffect } from 'react';
export const GenerateLuckyNumber = () => {
const [luckNo, setluckNo] = useState(1)
const changeCatName = () => {
setluckNo(Math.floor(Math.random() * 100))
}
return (
<>
<TouchableOpacity onPress={() => changeCatName()}>
<Text>Hello, Your Lucky Number is {luckNo}!</Text>
</TouchableOpacity>
</>
);
}
const showLuckyNumber = () => {
const [listLuckyNumber, setlistLuckyNumber] = useState()
//how to save all generated number from functional component to state in this component
return (
<>
<GenerateLuckyNumber/>
<GenerateLuckyNumber/>
<GenerateLuckyNumber/>
</>
);
}
export default showLuckyNumber;
Your component can provide an optional callback function as a prop, which it would invoke whenever it "generates a lucky number". For example:
export const GenerateLuckyNumber = ({onGenerate}) => {
And within the component:
const changeCatName = () => {
const luckyNumber = Math.floor(Math.random() * 100);
setluckNo(luckyNumber);
onGenerate && onGenerate(luckyNumber); // invoke the callback
}
Then any component which renders a <GenerateLuckyNumber/> can optionally supply it with a callback:
const showLuckyNumber = () => {
const [listLuckyNumber, setlistLuckyNumber] = useState()
const handleNewLuckyNumber = (num) => {
// do whatever you like with the newly generated number here
};
return (
<>
<GenerateLuckyNumber onGenerate={handleNewLuckyNumber}/>
<GenerateLuckyNumber onGenerate={handleNewLuckyNumber}/>
<GenerateLuckyNumber onGenerate={handleNewLuckyNumber}/>
</>
);
}

Create helper function for a (click handler) function to reuse in multiple React components

For a 'back' button I've created below (onClick) handler function in my React app.
const { length: historyLength, goBack, replace } = useHistory();
const handleBack = () => {
if (historyLength > 2) {
goBack();
} else {
// History length is 2 by default when nothing is pushed to history yet
// https://stackoverflow.com/questions/9564041/why-history-length-is-2-for-the-first-page
replace(HomePage);
}
};
Then I am passing the onClick handler to my child component like: <Button onClick={handleBack}/>
I am using this handleBack function in multiple places in my React app. Is it a good approach make it e.g. a helper function and how exactly?
I also don't see any issue with the code or using it as a utility callback.
Is it a good approach make it e.g. a helper function and how exactly?
Anytime you can make your code more DRY (Don't Repeat Yourself) it's generally a good thing. My personal rule-of-thumb is if I've written the same utility code a third time I'll spend a bit of time to refactor it into a common utility (and unit test!!).
I might suggest creating a custom hook to return the back handler.
Example:
import { useHistory } from 'react-router-dom';
const useBackHandler = () => {
const history = useHistory();
const handleBack = React.useCallback(() => {
const { length: historyLength, goBack, replace } = history;
if (historyLength > 2) {
goBack();
} else {
replace(HomePage);
}
}, []);
return handleBack;
};
export default useBackHandler;
Now you have a single hook to import and use.
import useBackHandler from '../path/to/useBackHandler';
...
const backHandler = useBackHandler();
...
<button type="button" onClick={backHandler}>Back?</button>
If you are needing this function in older class components, then you'll need a way to inject the handleBack as a prop. For this you can create a Higher Order Component.
Example:
import useBackHandler from '../path/to/useBackHandler';
const withBackHandler = Component => props => {
const backHandler = useBackHandler();
return <Component {...props} backHandler={backHandler} />;
};
export default withBackHandler;
To use, import withBackHandler and decorate a React component and access props.backHandler.
import withBackHandler from '../path/to/withBackHandler';
class MyComponent extends React.Component {
...
someFunction = () => {
...
this.props.backHandler();
}
...
}
export default withBackHandler(MyComponent);
#meez
Don't see why this wouldn't work. Just a couple of things: (a) I would add the event argument and e.preventDefault() within the function and (b) would be careful of the function name you are passing on the onClick property of your button: handleBackClick !== handleBack, you'll get an ReferenceError because of an undefined function.
Additionally, I also noticed that this can be achieved with native browser functions. Here's a snippet:
const { length: historyLength, back } = window.history;
const { replace } = window.location;
const handleBack = (e) => {
e.preventDefault();
if (historyLength > 2) {
back();
} else {
replace('homepageUrl');
}
};

Implement useSelector equivalent for React Context?

There's a bunch of articles out there that show how Redux can be replaced with context and hooks (see this one from Kent Dodds, for instance). The basic idea is to make your global state available through a context instead of putting it inside a Redux store. But there's one big problem with that approach: components that subscribe to the context will be rerendered whenever any change happens to the context, regardless of whether or not your component cares about the part of the state that just changed. For functional components, React-redux solves this problem with the useSelector hook. So my question is: can a hook like useSelector be created that would grab a piece of the context instead of the Redux store, would have the same signature as useSelector, and, just like useSelector, would only cause rerenders to the component when the "selected" part of the context has changed?
(note: this discussion on the React Github page suggests that it can't be done)
No, it's not possible. Any time you put a new context value into a provider, all consumers will re-render, even if they only need part of that context value.
That's specifically one of the reasons why we gave up on using context to propagate state updates in React-Redux v6, and switched back to using direct store subscriptions in v7.
There's a community-written React RFC to add selectors to context, but no indication the React team will actually pursue implementing that RFC at all.
As markerikson answers, it is not possible, but you can work around it without using external dependencies and without falling back to doing manual subscriptions.
As a workaround, you can let the component re-render, but skip the VDOM reconciliation by memoizing the returned React element with useMemo.
function Section(props) {
const partOfState = selectPartOfState(useContext(StateContext))
// Memoize the returned node
return useMemo(() => {
return <div>{partOfState}</div>
}, [partOfState])
}
This is because internally, when React diffs 2 versions of virtual DOM nodes, if it encountered the exact same reference, it will skip reconciling that node entirely.
I created a toolkit for managing state using ContextAPI. It provides useSelector (with autocomplete) as well as useDispatch.
The library is available here:
https://www.npmjs.com/package/react-context-toolkit
https://github.com/bergkvist/react-context-toolkit
It uses:
use-context-selector to avoid unneccesary rerenders.
createSlice from #reduxjs/toolkit to make the state more modular and to avoid boilerplate.
I've created this small package, react-use-context-selector, and it just does the job.
I used the same approach as used in Redux's useSelector. It also comes with type declarations and the return type matches the selector function's return type making it suitable for using in TS project.
function MyComponent() {
// This component will re-render only when the `name` within the context object changes.
const name = useContextSelector(context, value => value.name);
return <div>{name}</div>;
}
Here is my take on this problem:
I used the function as child pattern with useMemo to create a generic selector component:
import React, {
useContext,
useReducer,
createContext,
Reducer,
useMemo,
FC,
Dispatch
} from "react";
export function createStore<TState>(
rootReducer: Reducer<TState, any>,
initialState: TState
) {
const store = createContext({
state: initialState,
dispatch: (() => {}) as Dispatch<any>
});
const StoreProvider: FC = ({ children }) => {
const [state, dispatch] = useReducer(rootReducer, initialState);
return (
<store.Provider value={{ state, dispatch }}>{children}</store.Provider>
);
};
const Connect: FC<{
selector: (value: TState) => any;
children: (args: { dispatch: Dispatch<any>; state: any }) => any;
}> = ({ children, selector }) => {
const { state, dispatch } = useContext(store);
const selected = selector(state);
return useMemo(() => children({ state: selected, dispatch }), [
selected,
dispatch,
children
]);
};
return { StoreProvider, Connect };
}
Counter component:
import React, { Dispatch } from "react";
interface CounterProps {
name: string;
count: number;
dispatch: Dispatch<any>;
}
export function Counter({ name, count, dispatch }: CounterProps) {
console.count("rendered Counter " + name);
return (
<div>
<h1>
Counter {name}: {count}
</h1>
<button onClick={() => dispatch("INCREMENT_" + name)}>+</button>
</div>
);
}
Usage:
import React, { Reducer } from "react";
import { Counter } from "./counter";
import { createStore } from "./create-store";
import "./styles.css";
const initial = { counterA: 0, counterB: 0 };
const counterReducer: Reducer<typeof initial, any> = (state, action) => {
switch (action) {
case "INCREMENT_A": {
return { ...state, counterA: state.counterA + 1 };
}
case "INCREMENT_B": {
return { ...state, counterB: state.counterB + 1 };
}
default: {
return state;
}
}
};
const { Connect, StoreProvider } = createStore(counterReducer, initial);
export default function App() {
return (
<StoreProvider>
<div className="App">
<Connect selector={(state) => state.counterA}>
{({ dispatch, state }) => (
<Counter name="A" dispatch={dispatch} count={state} />
)}
</Connect>
<Connect selector={(state) => state.counterB}>
{({ dispatch, state }) => (
<Counter name="B" dispatch={dispatch} count={state} />
)}
</Connect>
</div>
</StoreProvider>
);
}
Working example: CodePen
Solution with external store (Redux or Zustand like approach) with new hook useSyncExternalStore comes with React 18.
For React 18: Define createStore and useStore functions:
import React, { useCallback } from "react";
import { useSyncExternalStore } from "react";
const createStore = (initialState) => {
let state = initialState;
const getState = () => state;
const listeners = new Set();
const setState = (fn) => {
state = fn(state);
listeners.forEach((l) => l());
};
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
return { getState, setState, subscribe };
};
const useStore = (store, selector) =>
useSyncExternalStore(
store.subscribe,
useCallback(() => selector(store.getState()), [store, selector])
);
Now use it :
const store = createStore({ count: 0, text: "hello" });
const Counter = () => {
const count = useStore(store, (state) => state.count);
const inc = () => {
store.setState((prev) => ({ ...prev, count: prev.count + 1 }));
};
return (
<div>
{count} <button onClick={inc}>+1</button>
</div>
);
};
For React 17 and any React version that supports hooks:
Option 1: You may use the external library (maintained by React team)
use-sync-external-store/shim :
import { useSyncExternalStore } from "use-sync-external-store/shim";
Option 2: If you don't want to add new library and don't care about concurency problems:
const createStore = (initialState) => {
let state = initialState;
const getState = () => state;
const listeners = new Set();
const setState = (fn) => {
state = fn(state);
listeners.forEach((l) => l());
}
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
}
return {getState, setState, subscribe}
}
const useStore = (store, selector) => {
const [state, setState] = useState(() => selector(store.getState()));
useEffect(() => {
const callback = () => setState(selector(store.getState()));
const unsubscribe = store.subscribe(callback);
callback();
return unsubscribe;
}, [store, selector]);
return state;
}
Sources:
A conference talk from Daishi Kato from React Conf 2021
A blog post about same conference talk by Chetan Gawai
Simple approach to prevent additional renders with HoC and React.memo:
const withContextProps = (WrappedComponent) => {
const MemoizedComponent = React.memo(WrappedComponent);
return (props) => {
const state = useContext(myContext);
const mySelectedState = state.a.b.c;
return (
<MemoizedComponent
{...props}
mySelectedState={mySelectedState} // inject your state here
/>
);
};
};
withContextProps(MyComponent)
I have made a library, react-context-slices, which can solve what you are looking for. The idea is to break the store or state in slices of state, that is, smaller objects, and create a context for each one. That library which I told you does this, exposes a function createSlice which accepts a reducer, initial state, name of the slice, and a function for creating the actions. You create as slices as you want ('todos', 'counter', etc) and integrate them in a unique interface easily, exposing at the end two custom hooks, useValues and useActions, which can 'attack' all the slices (that is, in your client components you do not use useTodosValues but useValues). The key is that useValues accepts a name of the slice, so would be equivalent to the useSelector from redux. The library use immer as redux does. It's a very tiny library which the key point is how is used, which is explained in the readme file. I have also made a post about it. The library exposes only two functions, createSlice and composeProviders.

How do I update an array using the useContext hook?

I've set a Context, using createContext, and I want it to update an array that will be used in different components. This array will receive the data fetched from an API (via Axios).
Here is the code:
Context.js
import React, { useState } from "react";
const HeroContext = React.createContext({});
const HeroProvider = props => {
const heroInformation = {
heroesContext: [],
feedHeroes: arrayFromAPI => {
setHeroesContext(...arrayFromAPI);
console.log();
}
};
const [heroesContext, setHeroesContext] = useState(heroInformation);
return (
<HeroContext.Provider value={heroesContext}>
{props.children}
</HeroContext.Provider>
);
};
export { HeroContext, HeroProvider };
See above that I created the context, but set nothing? Is it right? I've tried setting the same name for the array and function too (heroesContex and feedHeroes, respectively).
Component.js
import React, { useContext, useEffect } from "react";
import { HeroContext } from "../../context/HeroContext";
import defaultSearch from "../../services/api";
const HeroesList = () => {
const context = useContext(HeroContext);
console.log("Just the context", context);
useEffect(() => {
defaultSearch
.get()
.then(response => context.feedHeroes(response.data.data.results))
.then(console.log("Updated heroesContext: ", context.heroesContext));
}, []);
return (
//will return something
)
In the Component.js, I'm importing the defaultSearch, that is a call to the API that fetches the data I want to push to the array.
If you run the code right now, you'll see that it will console the context of one register in the Just the context. I didn't want it... My intention here was the fetch more registers. I have no idea why it is bringing just one register.
Anyway, doing all of this things I did above, it's not populating the array, and hence I can't use the array data in another component.
Does anyone know how to solve this? Where are my errors?
The issue is that you are declaring a piece of state to store an entire context object, but you are then setting that state equal to a single destructured array.
So you're initializing heroesContext to
const heroInformation = {
heroesContext: [],
feedHeroes: arrayFromAPI => {
setHeroesContext(...arrayFromAPI);
console.log();
}
};
But then replacing it with ...arrayFromAPI.
Also, you are not spreading the array properly. You need to spread it into a new array or else it will return the values separately: setHeroesContext([...arrayFromAPI]);
I would do something like this:
const HeroContext = React.createContext({});
const HeroProvider = props => {
const [heroes, setHeroes] = useState([]);
const heroContext = {
heroesContext: heroes,
feedHeroes: arrayFromAPI => {
setHeroes([...arrayFromAPI]);
}
};
return (
<HeroContext.Provider value={heroContext}>
{props.children}
</HeroContext.Provider>
);
};
export { HeroContext, HeroProvider };

Central State Management without Redux or Mobx?

Recently I contemplated the idea of having central state management in my React apps without using Redux or Mobx, instead opting to create something similar to the application class in Android. In any event, I implemented something similar to this:
Create a store folder and a file called store.js in it whose contents are:
// State
let state = {
users: {},
value: 0
};
// Stores references to component functions
let triggers = [];
// Subscription Methods
export const subscribe = trigger => {
triggers.push(trigger);
trigger();
}
export const unsubscribe = trigger => {
let pos = -1;
for (let i in triggers) {
if (triggers[i]===trigger) {
pos = i;
break;
}
}
if (pos!==-1) {
triggers.splice(pos, 1);
}
}
// Trigger Methods
let triggerAll = () => {
for (let trigger of triggers) {
trigger();
}
}
// State Interaction Methods
export const setUser = (name, description) => {
state.users[name] = description;
triggerAll();
}
export const removeUser = name => {
if (name in state.users) {
delete state.users[name];
}
triggerAll();
}
export const getAllUsers = () => {
return state.users;
}
export const getUser = name => {
if (!(name in state.users)) {
return null;
}
return state.users[name];
}
export const getValue = () => {
return state.value;
}
export const setValue = value => {
state.value = value;
triggerAll();
}
And connecting to this store in the following manner:
// External Modules
import React, { Component } from 'react';
import {Box, Text, Heading} from 'grommet';
// Store
import {subscribe, unsubscribe, getAllUsers} from '../../store/store';
class Users extends Component {
state = {
users: []
}
componentDidMount() {
subscribe(this.trigger); // push the trigger when the component mounts
}
componentWillUnmount() {
unsubscribe(this.trigger); // remove the trigger when the component is about to unmount
}
// function that gets triggered whenever state in store.js changes
trigger = () => {
let Users = getAllUsers();
let users = [];
for (let user in Users) {
users.push({
name: user,
description: Users[user]
});
}
this.setState({users});
}
render() {
return <Box align="center">
{this.state.users.map(user => {
return <Box
style={{cursor: "pointer"}}
width="500px"
background={{color: "#EBE7F3"}}
key={user.name}
round
pad="medium"
margin="medium"
onClick={() => this.props.history.push("/users/" + user.name)}>
<Heading margin={{top: "xsmall", left: "xsmall", right: "xsmall", bottom: "xsmall"}}>{user.name}</Heading>
<Text>{user.description}</Text>
</Box>
})}
</Box>;
}
}
export default Users;
Note. I've tested this pattern on a website and it works. Check it out here. And I apologize I am trying to keep the question concise for stackoverflow, I've provided a more detailed explanation of the pattern's implementation here
But anyway, my main question, what could be the possible reasons not to use this, since I assume if it was this simple, people wouldn't be using Redux or Mobx. Thanks in advance.
That's what Redux and MobX basically do, you are wrong in thinking that at their core concept they are much different. Their size and complexity came as a result of their effort to neutralize bugs and adapt to a vast variety of application cases. That's it. Although they might be approaching the task from different angles, but the central concept is just that. Maybe you should familiarize your self with what they actually do underneath.
Btw, you do not need to store redundant state in your component, if all you need is to trigger the update. You can just call forceUpdate() directly:
// function that gets triggered whenever state in store.js changes
trigger = () => {
this.forceUpdate();
}
That's similar to what Redux and MobX bindings for react do under the hood.

Resources