React UseContext with useReducer re-renders the whole app even after splitting - javascript

I am struggling to prevent my whole app getting re-rendered.I have even tried to split the context and memoize components but nothing helps me.
I have already read Use context Github issue this thread and a lot of other articles describing the right implementation but with no result.
I am using websockets to retrieve bitcoin value every second and it causes the whole app re-render on every second.
This is the App.tsx
<AppProvider>
<ThemeProvider theme={theme}>
<GlobalStyles />
<BrowserRouter>
<div className="App">
<AppRoot isBusy={!ready || !isConnected} />
</div>
</BrowserRouter>
</ThemeProvider>
</AppProvider>
This is the main context
import React, { createContext, useReducer, Dispatch } from 'react';
import { BTCReducerActions, BTCData, BTCPayload, btcReducer } from '../reducers/btc';
import { CommonReducerActions, CommonData, CommonPayload, commonReducer } from '../reducers/common';
import { UserReducerActions, UserData, UserPayload, userReducer } from '../reducers/user';
interface InitialStateType {
user: UserData;
btc: BTCData;
common: CommonData;
}
const initialState: InitialStateType = {
btc: {} as BTCData,
common: {},
user: {} as UserData,
};
export type ActionType = (
BTCReducerActions |
CommonReducerActions |
UserReducerActions |
);
type MainReducer = (state: InitialStateType, action: ActionType) => InitialStateType;
const AppContext = createContext<{
state: InitialStateType;
dispatch: Dispatch<ActionType>;
}>({
state: initialState,
dispatch: () => null,
});
const mainReducer: MainReducer = ({ btc, common, user}, action) => ({
btc: btcReducer<BTCPayload>(btc, action as BTCReducerActions),
user: userReducer<UserPayload>(user, action as UserReducerActions),
common: commonReducer<CommonPayload>(common, action as CommonReducerActions),
});
const AppProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(mainReducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
};
export { AppProvider, AppContext };
And this is the root.tsx
export const AppRoot = memo(({ isBusy }: Record<'isBusy', boolean>) => {
const {
dispatch,
state: {
common: { isMobile, soundSettings: { state, volume } },
user: { token },
},
} = useContext(AppContext);
const history = useHistory();
const location = useLocation();
useEffect(() => {
if (!isBusy) {
SocketManager.socketEmitter('query', {
header: { action: 'main::getInitialData' },
}, ({ token, user: { id } }) => dispatch({ type: UserActionTypes.UpdateUserData, payload: { token, id } }));
}
}, [isBusy]);
return (
<AppRootStyled className="fb horizontal">
<Content>
<AppHeader />
<div className="fb main-content">
<Switch>
<Route exact strict path={GAME_SCREEN} component={Roulette} />
<Route exact strict path={STATISTICS_SCREEN} component={Activity} />
<Route exact strict path={BET_HISTORY_SCREEN} render={props => <BetHistory roundHistory={roundHistory} {...props} />} />
<Route exact strict path={USER_BETS_SCREEN} component={MyBets} />
</Switch>
</div>
</Content>
{location.pathname !== GAME_SCREEN && (
<div className="go-home">
<div className="fb horizontal aCenter jCenter">
<Icon name="round-back-circled" onClick={() => history.push(GAME_SCREEN)} />
</div>
</div>
)}
</AppRootStyled>
);
});
Somewhere in the app I am using socket to get the btc data
useEffect(() => {
SocketManager.connectChannel('main::BTCData', (data) => {
dispatch({ type: BTCActionTypes.SetBTCData, payload: data });
});
}, []);
I have already tried to create a separate context by splitting the main state so the btc data has its own slice of the state, but again the Route components re-render every second.Also other components that use the context are re-rendering every second.
This is the context splitting implementation
const AppContext = {/ ** \}
const BtcContext = createContext<{btc: BTCData}>({ btc: {} as BTCData });
const AppProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(mainReducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
<BtcContext.Provider value={{btc}}>
{children}
</BtcContext.Provider>
</AppContext.Provider>
);
};
I know I am doing something wrong.I am using context as a state management service for the first time.Before I was using only redux and haven't faced this kind of problem.
I am already on it a few days and I need to solve the problem until tomorrow morning.
I have a lot of tables and other heavy components that shouldn't re-render so often.
UPDATED
I have tried this 2 options.
OPTION 1
<AppProvider>
<BtcProvider>
<ThemeProvider theme={theme}>
<GlobalStyles />
<BrowserRouter>
<div className="App">
<AppRoot isBusy={!ready || !isConnected} />
</div>
</BrowserRouter>
</ThemeProvider>
</BtcProvider>
</AppProvider>
const AppProvider: React.FC = ({ children }) => {
const [{ btc, ...mainStateData }, dispatch] = useReducer(mainReducer, initialState);
const mainState = useMemo(() => ({
state: mainStateData,
dispatch,
}), [mainStateData]);
return (
<AppContext.Provider value={mainState}>
{children}
</AppContext.Provider>
);
};
const BtcProvider: React.FC = ({ children }) => {
const [{ btc }, dispatch] = useReducer(mainReducer, initialState);
const btcData = useMemo(() => ({
btc,
dispatch,
}), [btc]);
return (
<BtcContext.Provider value={btcData}>
{children}
</BtcContext.Provider>
);
};
export { AppProvider, BtcProvider, AppContext, BtcContext };
And OPTION 2
const AppProvider: React.FC = ({ children }) => {
const [{ btc, ...mainStateData }, dispatch] = useReducer(mainReducer, initialState);
const mainState = useMemo(() => ({
state: mainStateData,
dispatch,
}), [mainStateData]);
const btcData = useMemo(() => ({
btc,
dispatch,
}), [btc]);
return (
<AppContext.Provider value={mainState}>
<BtcContext.Provider value={btcData}>
{children}
</BtcContext.Provider>
</AppContext.Provider>
);
};
<AppProvider>
<ThemeProvider theme={theme}>
<GlobalStyles />
<BrowserRouter>
<div className="App">
<AppRoot isBusy={!ready || !isConnected} />
</div>
</BrowserRouter>
</ThemeProvider>
The result is the same.The case is that the btc data is being changed every second and I need it to provide a lot of components, but to re-render only the components where I consume the data.

<AppContext.Provider value={{ state, dispatch }}>
Every time AppProvider renders, this code creates a new object { state, dispatch }. It may have the same contents as the previous object, but it's a different object, so react is forced to rerender any component that consumes this context. Similarly, you're making a brand new object for the BtcContext.Provider.
When providing an object, you need to make sure to memoize that object so it only changes when necessary:
const AppProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(mainReducer, initialState);
const appValue = useMemo(() => {
return { state, dispatch };
}, [state]);
const btcValue = useMemo(() => {
return { btc };
}, [btc]);
return (
<AppContext.Provider value={appValue}>
<BtcContext.Provider value={btcValue}>
{children}
</BtcContext.Provider>
</AppContext.Provider>
);
};

Related

How do I store a function call within Context

What I would like to be able to do is to initialize my context with a state and a function that updates that state.
For example, say I have the following:
export default function MyComponent () {
const MyContext = React.createContext()
const [myState, setMyState] = useState('1')
const contextValue = {
currentValue: myState,
setCurrentValue: (newValue) => setMyState(newValue)
}
return (
<MyContext.Provider value={contextValue}>
<MyContext.Consumer>
{e => <div onClick={() => e.setCurrentValue('2')}> Click me to change the value </div>}
{e.currentValue}
</MyContext.Consumer>
</MyContext.Provider>
)
}
The {e.currentValue} correctly outputs '1' at first, but when I click the button, nothing changes.
What I would expect is that e.setCurrentValue('2') would call setMyState('2'), which would update the state hook. This would then change the value of myState, changing the value of currentValue, and making '2' display.
What am I doing wrong?
You would want to return a fragment from the context as one JSX root.
Check here - https://playcode.io/931263/
import React, { createContext, useState } from "react";
export function App(props) {
const MyContext = React.createContext();
const [myState, setMyState] = useState("1");
const contextValue = {
currentValue: myState,
setCurrentValue: newValue => setMyState(newValue)
};
return (
<MyContext.Provider value={contextValue}>
<MyContext.Consumer>
{e => (
<>
<div onClick={() => e.setCurrentValue("2")}>
Click me to change the value
</div>
{e.currentValue}
</>
)}
</MyContext.Consumer>
</MyContext.Provider>
);
}
You're using e.currentValue outside of MyContext.Consumer context which does not have e, so it's throwing an error that e is not defined from e.currentValue.
You can wrap them up together under <MyContext.Consumer>{e => {}}</MyContext.Consumer>
function MyComponent() {
const MyContext = React.createContext();
const [myState, setMyState] = React.useState("1");
const contextValue = {
currentValue: myState,
setCurrentValue: (newValue) => setMyState(newValue),
};
return (
<MyContext.Provider value={contextValue}>
<MyContext.Consumer>
{(e) => (
<div>
<div onClick={() => e.setCurrentValue("2")}>
Click me to change the value
</div>
<div>{e.currentValue}</div>
</div>
)}
</MyContext.Consumer>
</MyContext.Provider>
);
}
ReactDOM.render(<MyComponent />, 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>

Why in AuthContext.Provider does't set data after login

If I set in context provider sample data, I see this data in all nested components.
But I need login to the account and in response, I get data about user for set in the global context and use in all components.
context/AuthProvider.tsx
const AuthContext = createContext<any>({});
export const AuthProvider = ({ children }: any) => {
const [auth, setAuth] = useState({});
return (
<>
<AuthContext.Provider value={{ auth, setAuth }}>{children}</AuthContext.Provider>
</>
);
};
hooks/useAuth.ts
const useAuth = () => {
return useContext(AuthContext);
};
export default useAuth;
index.tsx
import { AuthProvider } from './context/AuthProvider';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>
);
I have App with BrowserRouter logic for not logged users redirect to login. If logged, so go to the Homepage.
components/App/App.tsx
const AppContainer: FC<any> = () => {
const { token } = useToken();
return (
<>
<div className={'wrapper'}>
<BrowserRouter>
{!token ? <LoggedOutRoutes /> : <LoggedInRoutes />}
</BrowserRouter>
</div>
</>
);
};
const LoggedOutRoutes: FC<any> = () => (
<Switch>
<Route path="/" exact={true}>
<Login />
</Route>
<Redirect from={'*'} to={'/'} />
</Switch>
);
const LoggedInRoutes: FC = () => (
<Switch>
<Route path="/" exact={true} component={Homepage} />
</Switch>
);
In login component sending request with data and I getting access_token and user data. Now I need set user data in useAuth hook.
const Login: FC<any> = () => {
const { setToken } = useToken();
const { setAuth } = useAuth()
const handleSubmit = async (event: any) => {
event.preventDefault();
const res = await API.login({
login,
password
});
const { access_token, user } = res;
setToken(access_token);
setAuth(user);
window.location.reload();
};
return (
<form onClick={handleSubmit}>
// ...There i have submit form, not interesting
</form>
);
};
After reloading the page, my page will be Homepage where I won't use my context data from the provider but I have an empty object, why?
The problem is window.location.reload. Any SPA will not retain data after a page refresh by default.
Now if you still want to persist that information even after page reload, i recommend to persist that info in localStorage. So something like this should work.
export const AuthProvider = ({ children }: any) => {
const [auth, setAuth] = useState(localStorage.get('some-key') || {});
const updateAuth = (auth) => {
localStorage.set('some-key', auth);
setAuth(auth);
}
return (
<>
<AuthContext.Provider value={{ auth, updateAuth }}>{children}</AuthContext.Provider>
</>
);
};

What are the 3 core issues in the React Suspense code snippet?

I participated in a trihackathon last week, and they gave us a code snippet asking what are the 3 errors in the code that violate principles of React Suspense. Can anyone figure it out?
import { Suspense, useState, useEffect } from 'react';
const SuspensefulUserProfile = ({ userId }) => {
const [data, setData] = useState({});
useEffect(() => {
fetchUserProfile(userId).then((profile) => setData(profile));
}, [userId, setData])
return (
<Suspense>
<UserProfile data={data} />
</Suspense>
);
};
const UserProfile = ({ data }) => {
return (
<>
<h1>{data.name}</h1>
<h2>{data.email}</h2>
</>
);
};
const UserProfileList = () => (
<>
<SuspensefulUserProfile userId={1} />
<SuspensefulUserProfile userId={2} />
<SuspensefulUserProfile userId={3} />
</>
);

How to pass props to route components when using useRoutes?

I'm trying out the new useRoutes hook from react-router-dom and it seems to be pretty interesting. The only problem, is that I can't figure out how I would pass props down to the components.
Before, I'd have a Route component, and I would select parts of local or global state there and pass it on, but how would I do it with useRoutes?
In the below sandbox, I'm trying to change the background based on the boolean value of isLoading. How would I pass isLoading on?
Edit
Here's the code:
import React from "react";
import { BrowserRouter as Router, Outlet, useRoutes } from "react-router-dom";
const Main = ({ isLoading }) => (
<div
style={{
height: "40vh",
width: "50vw",
backgroundColor: isLoading ? "red" : "pink"
}}
>
<Outlet />
</div>
);
const routes = [
{
path: "/",
element: <Main />
}
];
const App = ({ isLoading }) => {
const routing = useRoutes(routes);
return (
<>
{routing}
{JSON.stringify(isLoading)}
</>
);
};
export default function Entry() {
const [isLoading, setIsLoading] = React.useState(false);
React.useEffect(() => {
setInterval(() => {
setIsLoading(!isLoading);
}, 3000);
}, [isLoading]);
return (
<Router>
<App isLoading={isLoading} />
</Router>
);
}
Edit
I've considered passing in an isLoading argument to routes, but I feel like that won't be an efficient approach, because the whole tree will rerender at any route, regardless of whether or not it depends on isLoading or doesn't. Would a better approach be to use Switch and a custom Route component for routes that depend on isLoading and just use useSelector in that custom Route component?
I think this would work:
const routes = (props) => [
{
path: "/",
element: <Main {...props} />
}
];
const App = ({ isLoading }) => {
const routing = useRoutes(routes({isLoading}));
return (
<>
{routing}
{JSON.stringify(isLoading)}
</>
);
};
Instead of passing the array, you can create a function that returns a constructed array to the useRoutes hook:
const routes = (isLoading) => [
{
path: "/",
element: <Main isLoading={isLoading} />
}
];
code sandbox:
https://codesandbox.io/s/dark-dream-hx1y9
more pretty would be:
const routes = (props) => [
{
"/": () => <Main {...props} />
}
];
const App = ({ isLoading }) => {
const routing = useRoutes(routes(isLoading));
return (
<>
{routing}
{JSON.stringify(isLoading)}
</>
)
};

How to pass data to {props.children}

On my follow up question from here : How to pass data from child to parent component using react hooks
I have another issue.
Below is the component structure
export const Parent: React.FC<Props> = (props) => {
const [disabled, setDisabled] = React.useState(false);
const createContent = (): JSX.Element => {
return (
<Authorization>
{<ErrorPanel message={errorMessage} setDisabled={setDisabled}/>}
<MyChildComponent/>
</<Authorization>
);
}
return (
<Button onClick={onSubmit} disabled={disabled}>My Button</Button>
{createContent()}
);
};
const Authorization: React.FC<Props> = (props) => {
const [disabled, setDisabled] = React.useState(false);
const render = (errorMessage : JSX.Element): JSX.Element => {
return (
<>
{<ErrorPanel message={errorMessage} setDisabled={setDisabled}/>}
</>
);
};
return (
<>
<PageLoader
queryResult={apiQuery}
renderPage={render}
/>
{props.children}
</>
);
};
How do I pass the disabled state value from Authorization component to my child which is invoked by
{props.children}
I tried React.cloneElement & React.createContext but I'm not able to get the value disabled to the MyChildComponent. I could see the value for disabled as true once the errorMessage is set through the ErrorPanel in the Authorization component.
Do I need to have React.useEffect in the Authorization Component?
What am I missing here?
You need to use React.Children API with React.cloneElement:
const Authorization = ({ children }) => {
const [disabled, setDisabled] = React.useState(false);
const render = (errorMessage) => {
return (
<>{<ErrorPanel message={errorMessage} setDisabled={setDisabled} />}</>
);
};
return (
<>
<PageLoader queryResult={apiQuery} renderPage={render} />
{React.Children.map(children, (child) =>
React.cloneElement(child, { disabled })
)}
</>
);
};
// |
// v
// It will inject `disabled` prop to every component's child:
<>
<ErrorPanel
disabled={disabled}
message={errorMessage}
setDisabled={setDisabled}
/>
<MyChildComponent disabled={disabled} />
</>
You can make use of React.cloneElement to React.Children.map to pass on the disabled prop to the immediate children components
const Authorization: React.FC<Props> = (props) => {
const [disabled, setDisabled] = React.useState(false);
const render = (errorMessage : JSX.Element): JSX.Element => {
return (
<>
{<ErrorPanel message={errorMessage} setDisabled={setDisabled}/>}
</>
);
};
return (
<>
<PageLoader
queryResult={apiQuery}
renderPage={render}
/>
{React.Children.map(props.children, child => {
return React.cloneElement(child, { disabled })
})}
</>
);
};
UPDATE:
Since you wish to update the parent state to, you should store the state and parent and update it there itself, instead of storing the state in child component too.
export const Parent: React.FC<Props> = (props) => {
const [disabled, setDisabled] = React.useState(false);
const createContent = (): JSX.Element => {
return (
<Authorization setDisabled={setDisabled}>
{<ErrorPanel message={errorMessage} disabled={disabled} setDisabled={setDisabled}/>}
<MyChildComponent disabled={disabled}/>
</<Authorization>
);
}
return (
<Button onClick={onSubmit} disabled={disabled}>My Button</Button>
{createContent()}
);
};
const Authorization: React.FC<Props> = (props) => {
const render = (errorMessage : JSX.Element): JSX.Element => {
return (
<>
{<ErrorPanel message={errorMessage} disabled={props.disabled} setDisabled={props.setDisabled}/>}
</>
);
};
return (
<>
<PageLoader
queryResult={apiQuery}
renderPage={render}
/>
{props.children}
</>
);
};

Categories

Resources