How to prevent unnecessary renders in a Component using useContext? - javascript

I would like to know how can I avoid unnecessary updates on a child component that does not receive props, but uses useContext.
I've added React.memo to the Child2 component for me to make some demo tests.
As you can see, when I change the input value which uses context to pass the data to child1, Child2 does not re-render, as React.memo prevents this behavior.
How can I prevent unnecessary renders in Child1 as well? I know that React.memo won't work as it needs props, and with context this will not happen, as no props are being passed down to this component
App.js
import React, {useState} from 'react';
import './App.css';
import Child1 from "./Child1";
import Child2 from "./Child2";
import SillyContext from './context'
function App() {
const [name, setName] = useState('Radha');
const [count, setCount] = useState(0)
const increaseCount = () => {
setCount(prevState => prevState + 1)
}
return (
<div className="App">
<Child2 count={count}/>
<button onClick={increaseCount}>Increase</button>
<input type="text" value={name} onChange={(event => setName(event.target.value))}/>
<SillyContext.Provider value={{name}}>
<Child1/>
</SillyContext.Provider>
</div>
);
}
export default App;
Child1
import React, {useContext} from "react";
import SillyContext from './context'
export default function Child1() {
const sillyContext = useContext(SillyContext)
console.log('[Child 1 ran]')
return (
<div>
<div>[Child 1]: {sillyContext.name}</div>
</div>
)
}
Child2
import React from 'react'
export default React.memo(function Child2(props) {
console.log('[Child2 Ran!]')
return <div>[Child2] - Count: {props.count}</div>
})

The major problem due to which Child1 re-renders when count is updated is because you are passing a new object reference to Context Provider everytime.
Also If the App component re-renders, all element rendered within it re-render, unless they implement memoization or are PureComponent or use shouldComponentUpdate
You can make 2 changes to fix your re-rendering
wrap Child1 with React.memo
Use useMemo to memoize the object passed as value to provider
App.js
function App() {
const [name, setName] = useState("Radha");
const [count, setCount] = useState(0);
const increaseCount = () => {
setCount(prevState => prevState + 1);
};
const value = useMemo(() => ({ name }), [name]);
return (
<div className="App">
<Child2 count={count} />
<button onClick={increaseCount}>Increase</button>
<input
type="text"
value={name}
onChange={event => setName(event.target.value)}
/>
<SillyContext.Provider value={value}>
<Child1 />
</SillyContext.Provider>
</div>
);
}
Child2
const Child2 = React.memo(function(props) {
console.log("[Child2 Ran!]");
return <div>[Child2] - Count: {props.count}</div>;
});
Child 1
const Child1 = React.memo(function() {
const sillyContext = useContext(SillyContext);
console.log("[Child 1 ran]");
return (
<div>
<div>[Child 1]: {sillyContext.name}</div>
</div>
);
});
Working demo

You can use useMemo inside Child1 component. It doesn't solve rerendering problem, but it is some kind of improvement.
Following snippet is third variant proposed by React core dev
we could make our code a bit more verbose but keep it in a
single component by wrapping return value in useMemo and specifying
its dependencies. Our component would still re-execute, but React
wouldn't re-render the child tree if all useMemo inputs are the same.
const { useState, useEffect, createContext, useContext, useMemo } = React;
const SillyContext = createContext();
const Child2 = React.memo(({count}) => {
return <div>Count: {count}</div>
})
const Child1 = () => {
const {name} = useContext(SillyContext);
return useMemo(() => {
console.log('Child1');
return <div>
<div>Child 1: {name}</div>
</div>}, [name])
}
const App = () => {
const [count, setCount] = useState(0);
const increaseCount = () => {
setCount(prevState => prevState + 1)
}
return <div>
<Child2 count={count}/>
<button onClick={increaseCount}>Increase</button>
<Child1/>
</div>
}
ReactDOM.render(
<SillyContext.Provider value={{name: 'test'}}>
<App />
</SillyContext.Provider>,
document.getElementById('root')
);
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>

Related

React passing props to other components

Hello I am having trouble passing props between components. I can't share the exact code so I made a simplified version. I am not getting any console errors, though login is obviously 'undefined' Any insight is appreciated!
App.js
import React, { useState } from "react";
function App() {
const [login, setLogin] = useState('Jpm91297');
const changeState = () => {
const newLogin = document.getElementById('loginData').value;
setLogin(newLogin);
}
return (
<>
<h1>Fancy API Call!!!</h1>
<form onSubmit={() => changeState()}>
<input type='text' id='loginData'></input>
<button>Submit</button>
</form>
</>
);
}
export default App;
Api.Js
import React, {useEffect, useState} from "react";
const Api = ( { login } ) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch(`https://api.github.com/users/${login}`)
.then((res) => res.json())
.then(setData);
}, []);
if (data) {
return <div>{JSON.stringify(data)}</div>
}
return <div>No data Avail</div>
}
export default Api;
Index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import Api from './api'
ReactDOM.render(
<>
<App />
<Api />
</>,
document.getElementById('root')
);
You are not preventing the default form action from occurring. This reloads the app.
You should lift the login state to the common parent of App and Api so it can be passed down as a prop. See Lifting State Up.
Example:
index.js
Move the login state to a parent component so that it can be passed down as props to the children components that care about it.
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import Api from './api';
const Root = () => {
const [login, setLogin] = useState('Jpm91297');
return (
<>
<App setLogin={setLogin} />
<Api login={login} />
</>
);
};
ReactDOM.render(
<Root />,
document.getElementById('root')
);
App
Pass the changeState callback directly as the form element's onSubmit handler and prevent the default action. Access the form field from the onSubmit event object.
function App({ setLogin }) {
const changeState = (event) => {
event.preventDefault();
const newLogin = event.target.loginData.value;
setLogin(newLogin);
}
return (
<>
<h1>Fancy API Call!!!</h1>
<form onSubmit={changeState}>
<input type='text' id='loginData'></input>
<button type="submit">Submit</button>
</form>
</>
);
}
Api
const Api = ({ login }) => {
const [data, setData] = useState(null);
useEffect(() => {
fetch(`https://api.github.com/users/${login}`)
.then((res) => res.json())
.then(setData);
}, [login]); // <-- add login dependency so fetch is made when login changes
if (data) {
return <div>{JSON.stringify(data)}</div>;
}
return <div>No data Avail</div>;
};

props are not working when using map method in react.js

props are passing fine when we are passing them as a whole array of objects but it is not working when I am passing the props by traversing through the array using map function.
import { React, useEffect, useState } from "react";
import axios from "axios";
import "./Home.css";
import Cardimg from "./Cardimg";
const Home = props => {
return (
<>
<div className="header">PHOTO GALLERY</div>
<div className="photos">
{props.data?.map(e => {
<Cardimg data={e.ImgUrl}></Cardimg>;
})}
</div>
</>
);
};
export default Home;
in the above code props are passing when I am passing manually in Cardimg component...but as soon as I start using map then it doesn't work...like the props are not reaching the component.
below is my Cardimg component
import React from 'react'
const Cardimg = (props) => {
console.log(props.data);
return (
<div>{props.data}</div>
)
}
export default Cardimg
You need to return the Cardimg component inside map callback function.
Either like this
{
props.data?.map(e => {
return <Cardimg data={e.ImgUrl}></Cardimg>;
});
}
Or like this
{
props.data?.map(e => <Cardimg data={e.ImgUrl}></Cardimg>)
}

Using React Hooks for Data Provider and Data Context

I am currently reworking my DataProvider, updating it from a class component to a functional component with React Hooks.
I believe my issue is in the way I am setting up my context consumer but I haven't found a good way to test this.
DataProvider.js
import React, { createContext } from "react";
const DataContext = createContext();
export const DataProvider = (props) => {
const [test, setTest] = React.useState("Hello");
return (
<DataContext.Provider value={test}>{props.children}</DataContext.Provider>
);
};
export const withContext = (Component) => {
return function DataContextComponent(props) {
return (
<DataContext.Consumer>
{(globalState) => <Component {...globalState} {...props} />}
</DataContext.Consumer>
);
};
};
So my withContext function should receive a component and pass it the props of the Context Provider.
I try to pull in my test state into a component.
import React from "react";
import style from "./DesktopAboutUs.module.css";
import { withContext } from "../../DataProvider";
const DesktopAboutUs = ({ test }) => {
return (
<div className={style.app}>
<div>{test}</div>
</div>
);
};
export default withContext(DesktopAboutUs);
No data is showing up for test. To me this indicates that my withContext function is not properly receiving props from the Provider.
Because you passed value={test}, globalState is a string, not an object with a test property.
Either of these solutions will result in what you expected:
Pass an object to the value prop of DataContext.Provider using value={{ test }} instead of value={test} if you intend globalState to contain multiple props.
Pass globalState to the test prop of Component using test={globalState} instead of {...globalState} if you do not intend globalState to contain multiple props.
const DataContext = React.createContext();
const DataProvider = (props) => {
const [test, setTest] = React.useState("Hello");
return (
<DataContext.Provider value={{ test }}>
{props.children}
</DataContext.Provider>
);
};
const withContext = (Component) => (props) => (
<DataContext.Consumer>
{(globalState) => <Component {...globalState} {...props} />}
</DataContext.Consumer>
);
const DesktopAboutUs = ({ test }) => (
<div>{test}</div>
);
const DesktopAboutUsWithContext = withContext(DesktopAboutUs);
ReactDOM.render(
<DataProvider>
<DesktopAboutUsWithContext />
</DataProvider>,
document.querySelector('main')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<main></main>

React hooks and context api localstorage on refresh

In my SPA, I am utilizing react hooks and context API. I need to persist the current state of the component view rendered using the context API so that I can implement the global component conditional rendering through the application.
I have two views on a single dashboard page: overview & detail. The button triggers the global state change and the view should be fixed on the state value even on page refresh.
Here's my code snippets:
AppRoutes file
import React, { useState } from "react";
import { Router, Route, Switch } from "react-router-dom";
import history from "../utils/history";
import { PyramidProvider } from "../context/pyramidContext";
import Dashboard from "../pages/dashboard/Dashboard";
const AppRoutes = () => {
return (
<div>
<React.Suspense fallback={<span>Loading...</span>}>
<Router history={history}>
<Switch>
<PyramidProvider>
<Route path="/" component={Dashboard} />
</PyramidProvider>
</Switch>
</Router>
</React.Suspense>
</div>
);
};
export default AppRoutes;
Dashboard page
import React, { useState, useEffect, useContext } from "react";
import { PyramidContext } from "../../context/pyramidContext";
import PyramidDetail from "../../components/pyramidUI/pyramidDetail";
import PyramidOverview from "../../components/pyramidUI/pyramidOverview";
const Dashboard = (props) => {
const { info, setInfo } = useContext(PyramidContext);
return (
<React.Fragment>
{info.uiname === "overview" ? <PyramidOverview /> : <PyramidDetail />}
</React.Fragment>
);
};
export default Dashboard;
Overview component
import React, { useState, useContext } from "react";
import { PyramidContext } from "../../context/pyramidContext";
const Overview = (props) => {
const { info, setInfo } = useContext(PyramidContext);
return (
<div className="d-flex flex-column dashboard_wrap">
<main>
<div className="d-flex">
<button
onClick={() => setInfo({ uiname: "detail", pyramidvalue: 1 })}
>
change view
</button>
</div>
</main>
</div>
);
};
export default Overview;
Detail component
import React, { useContext } from "react";
import { PyramidContext } from "../../context/pyramidContext";
// import axios from "axios";
const Detail = (props) => {
const { info, setInfo } = useContext(PyramidContext);
return (
<div className="d-flex flex-column dashboard_wrap">
<h2>Detail View</h2>
<div>
<button
type="button"
onClick={() => setInfo({ uiname: "overview", pyramidvalue: 0 })}
>
Back
</button>
</div>
</div>
);
};
export default Detail;
Context File
import React, { createContext, useEffect, useReducer } from "react";
let reducer = (info, newInfo) => {
return { ...info, ...newInfo };
};
const initialState = {
uiname: "overview",
pyramidvalue: 0,
};
const localState = JSON.parse(localStorage.getItem("pyramidcontent"));
const PyramidContext = createContext();
function PyramidProvider(props) {
const [info, setInfo] = useReducer(reducer, initialState || localState);
useEffect(() => {
localStorage.setItem("pyramidcontent", JSON.stringify(info));
}, [info]);
return (
<PyramidContext.Provider
value={{
info,
setInfo,
}}
>
{props.children}
</PyramidContext.Provider>
);
}
export { PyramidContext, PyramidProvider };
I click the button to render a detail view and soon as the page is refreshed, the component changes its view to overview instead of sticking around to detail. I checked the local storage values, and it is being updated properly, but still, the component view does not persist as per the value.
I am unable to understand where I am doing wrong, any help to resolve this issue, please? Thanks in advance.
You're never using the value of localStage in your info state,
you should replace your code with:
const [info, setInfo] = useReducer(reducer, localState || initialState);

Question around using useContext and useReducer hooks

I am trying to practice using hooks and i am not able to wrap my head around it.
I have a single component MessageBoard component that reads the data from state which just displays a simple list of messages.
I am passing down the dispatch and state via createContext so that the child components can consume it, which in-turn uses useContext in the child components to read the value.
When the page is refreshed, I expect to see the initial UI but it fails to render that the value in the context is undefined. I have already provided the initial state to the reducer when initializing it.
App.js
import React from "react";
import MessageBoard from "./MessageBoard";
import MessagesContext from "../context/MessagesContext";
function App() {
return (
<div>
<MessagesContext>
<h2>Reaction</h2>
<hr />
<MessageBoard />
</MessagesContext>
</div>
);
}
export default App;
MessageBoard.js
import React, { useContext } from "react";
import MessagesContext from "../context/MessagesContext";
function MessageBoard(props) {
const { state } = useContext(MessagesContext);
return (
<div>
{state.messages.map(message => {
return (
<div key={message.id}>
<h4>{new Date(message.timestamp).toLocaleDateString()}</h4>
<p>{message.text}</p>
<hr />
</div>
);
})}
</div>
);
}
export default MessageBoard;
MessagesContext.js
import React, { createContext, useReducer } from "react";
import reducer, { initialState } from "../state/reducer";
export default function MessagesContext(props) {
const Context = createContext(null);
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider
value={{
dispatch,
state
}}
>
{props.children}
</Context.Provider>
);
}
Broken Example - https://codesandbox.io/s/black-dust-13kj2
Instead if I change the MessagesContext file a bit and instead the Provider is directly injected into the App, it works as expected. Wondering what I have misunderstood here and what might be going on ?
MessagesContext.js
import { createContext } from "react";
export default createContext(null);
App.js
import React, { useReducer } from "react";
import reducer, { initialState } from "../state/reducer";
import PublishMessage from "./PublishMessage";
import MessageBoard from "./MessageBoard";
import MessagesContext from "../context/MessagesContext";
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<MessagesContext.Provider
value={{
dispatch,
state
}}
>
<h2>Reaction</h2>
<hr />
<PublishMessage />
<hr />
<MessageBoard />
</MessagesContext.Provider>
</div>
);
}
export default App;
Working Example - https://codesandbox.io/s/mystifying-meitner-vzhok
useContext accepts a context object (the value returned from React.createContext) and returns the current context value for that context.
const MyContext = createContext(null);
const value = useContext(MyContext);
// MessagesContext Not a contex object.
const { state } = useContext(MessagesContext);
In the first example:
// export the context.
export const Context = createContext(null);
export default function MessagesContext(props) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider
value={{
dispatch,
state
}}
>
{props.children}
</Context.Provider>
);
}
and then use it:
import { Context } from '../context/MessagesContext';
function MessageBoard() {
const { state } = useContext(Context);
...
}
Working broken example:

Categories

Resources