i'm having a table component for displaying some data. After dispatching an action the table data in the state are channging. However my table component is not updated. It is updated only when i click on another radio button in another row of my table. I want my component to rerender when the data are changed. Here is my code:
const mapStateToProps = state => ({
evaluationData: evaluationResultsSelector(state)
});
const mapDispatchToProps = dispatch => ({
setSelectedEvaluationRecord: record =>
dispatch(setSelectedEvaluationRecord(record))
});
export default connect(mapStateToProps,
mapDispatchToProps
EvaluationDataTable,
);
and my component is this:
import React from 'react';
import Table from 'antd/lib/table';
import 'antd/lib/table/style/css';
import "antd/dist/antd.css";
import { columnEvaluation } from './evaluationDataStructure';
class EvaluationDataTable extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedRowKeys: [0], // Check here to configure the default column
};
}
// shouldComponentUpdate(prevProps, prevState) {
// return (prevProps.results !== this.props.results || prevState.selectedRowKeys !== this.state.selectedRowKeys);
// }
onRowChange = selectedRowKeys => {
if (selectedRowKeys.length > 1) {
const lastSelectedRowIndex = [...selectedRowKeys].pop();
this.setState({ selectedRowKeys: lastSelectedRowIndex });
}
this.setState({ selectedRowKeys });
};
onRowSelect = (record) => {
this.props.setSelectedEvaluationRecord(record)
};
render() {
const { selectedRowKeys } = this.state;
const rowSelection = {
type: 'radio',
selectedRowKeys,
onChange: this.onRowChange,
onSelect: this.onRowSelect
};
return (
<React.Fragment>
<div style={{ marginBottom: 16 }} />
<Table
rowSelection={rowSelection}
columns={columnEvaluation}
dataSource={this.props.evaluationData}
/>
</React.Fragment>
);
}
}
export default EvaluationDataTable;
When i click in another row the table is rerendered as my setState is triggered but when the data are channged the table is not rerendered. Only when i click in another row. How to deal with it? Thanks a lot
Also my reducer which mutates the table is this:
case ACTION_TYPES.EDIT_EVALUATION_RESULTS: {
const evaluationResults = state.evaluationResults;
const editedRecord = action.payload.editedEvaluationData;
evaluationResults.forEach((item, i) => {
if (item.id === editedRecord.id) {
evaluationResults[i] = editedRecord;
}
});
return {
...state,
evaluationResults
};
}
Problem was here as OP has already deduced.
const evaluationResults = state.evaluationResults;
This was causing a state-mutation which goes against Redux principles. Although the state values were being updated in OP's proceeding code, the changes were being made to the same, initial object in reference. Redux does not register it as a new-state so it found no need to re-render our component. To get your connected-component to re-render we need a completely new redux-state.
To achieve this, we need to create a brand-new copy of evaluationResults like so and then the OP's feature will work as expected:
const evaluationResults = [...state.evaluationResults];
Related
I have a basic job board application. An API is called within the redux store (using thunk function) and initial job results are then saved in redux store.
Ref: https://redux.js.org/tutorials/essentials/part-5-async-logic
These initial Jobs are stored in redux store (and not in local component state), as I need to access these initial job results in other components as well
There are also three filters that can be applied to these initial jobs (Jobs can be filtered by location, team and commitment) I've put these filters inside the redux store as well. (Actions are triggered from
Filter UI component to update the current applied filters, and multiple filters can be active at one time)
The Filter UI component pretty much just renders a <Select> element with a handleChange function which causes the filters to update in the redux store, something like this:
Basic Filter UI Component which dispatches action :
<Select
name={name}
value={value}
onChange={handleChange}
></Select>
// ... omit some code ...
const handleChange = (event) => {
const { name } = event.target;
switch (name) {
case 'location':
dispatch(changeLocationFilter(event.target))
break;
case 'team':
dispatch(changeTeamFilter(event.target))
break;
case 'commitment':
dispatch(changeCommitmentFilter(event.target))
break;
}
}
Here is my filtersSlice in redux, which update the redux state when filters are applied:
import { createSlice } from "#reduxjs/toolkit";
import { ALL_LOCATIONS, ALL_TEAMS, ALL_COMMITMENTS } from '../constants'
const initialState = {
location: ALL_LOCATIONS,
team: ALL_TEAMS,
commitment: ALL_COMMITMENTS
};
export const filtersSlice = createSlice({
name: "filters",
initialState,
reducers: {
changeLocationFilter: (state, action) => {
const { payload: { value: locationValue } } = action;
state.location = locationValue;
},
changeTeamFilter: (state, action) => {
const { payload: { value: teamValue } } = action;
state.team = teamValue;
},
changeCommitmentFilter: (state, action) => {
const { payload: { value: commitmentValue } } = action;
state.commitment = commitmentValue;
}
}
});
// Action creators are generated for each case reducer function
export const { changeLocationFilter, changeTeamFilter, changeCommitmentFilter } = filtersSlice.actions;
export default filtersSlice.reducer;
Every time those filters change, I'm using a memoized createSelector function to get those updated filters, then I'm filtering my jobs locally within my JobContainer component
Ref:
https://redux.js.org/tutorials/essentials/part-6-performance-normalization
Ref:
https://redux-toolkit.js.org/api/createSelector
I am not updating the jobs in the redux store (From initial jobs to filtered jobs) because after doing some reading, it seems that when it comes to filtering data, the generally accepted best practice is to do this via derived state, and there is no need to put this inside component state or redux store state -
Ref:
What is the best way to filter data in React?
Here is some code to illustrate my example further:
Here is my JobsContainer component, which get the initial jobs and the filters from the redux store, and then filters the jobs locally:
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { createSelector } from "reselect";
import Job from "../../components/Job";
import { ALL_LOCATIONS, ALL_TEAMS, ALL_COMMITMENTS } from '../../constants'
import { fetchReduxJobs, selectAllReduxJobs } from '../../redux/reduxJobs'
const JobsContainer = () => {
const dispatch = useDispatch()
const reduxJobsStatus = useSelector(state => state.reduxJobs.status);
let reduxJobs = useSelector(selectAllReduxJobs); // GET INITIAL JOBS FROM REDUX STATE HERE
const filterState = useSelector((state) => state.filters); // GET FILTERS FROM REDUX STATE HERE
const selectLocation = filterState => filterState.location
const selectTeam = filterState => filterState.team
const selectCommitment = filterState => filterState.commitment
// CREATE MEMOIZED FUNCTION USING CREATESELECTOR, AND RUN A FILTER ON THE JOBS
// WHENEVER FILTERS CHANGE IN REDUX STORE
const selectFilters = createSelector([selectLocation, selectTeam, selectCommitment], (location, team, commitment) => {
let tempReduxJobs = reduxJobs;
tempReduxJobs = tempReduxJobs.filter((filteredJob) => {
return (
(location === ALL_LOCATIONS ? filteredJob : filteredJob.categories.location === location) &&
(commitment === ALL_COMMITMENTS ? filteredJob : filteredJob.categories.commitment === commitment) &&
(team === ALL_TEAMS ? filteredJob : filteredJob.categories.team === team)
)
})
return tempReduxJobs;
})
reduxJobs = selectFilters(filterState); // UPDATE JOBS HERE WHEN FILTERS CHANGE
let content;
if (reduxJobsStatus === 'loading') {
content = "Loading..."
} else if (reduxJobsStatus === 'succeeded') {
// JUST MODIFYING MY JOBS A BIT HERE BEFORE RENDERING THEM
let groupedReduxJobs = reduxJobs.reduce(function (groupedObj, job) {
const { categories: { team } } = job;
if (!groupedObj[team]) {
groupedObj[team] = []
}
groupedObj[team].push(job)
return groupedObj
}, {})
// THIS IS HOW I RENDER MY JOBS HERE AFTER MODIFYING THEM
content = Object.keys(groupedReduxJobs).map((teamName, index) => (
<div key={index}>
<div className="job-team-heading">{teamName}</div>
{groupedReduxJobs[teamName].map((job) =>
(<Job jobDetails={job} key={job.id} />))
}
</div>
))
// return groupedObj
} else if (reduxJobsStatus === 'failed') {
content = <div>{error}</div>
}
useEffect(() => {
if (reduxJobsStatus === 'idle') {
dispatch(fetchReduxJobs())
}
}, [reduxJobsStatus, dispatch])
return (
<JobsContainerStyles>
<div>{content}</div>
</JobsContainerStyles>
);
}
export default JobsContainer;
Something about how Im updating my jobs after the filters change (inside JobsContainer) using my selectFilters function ie the line:
reduxJobs = selectFilters(filterState);
Seems off. (Note: as you can see, I am modifying the data a bit before rendering as well - see groupedReduxJobs)
I wouldn't be as confused if I was to update the redux store with the filtered jobs after the filter is applied, but as I mentioned, reading into this topic suggests filtered data should generally be kept as derived state, and not in redux store. This is what I am confused about.
Can someone provide some constructive criticism on how I'm doing this please ? Or is the way Im doing this currently a good way to go about solving this problem.
To clarify, this is all working as written here .. but I'm not sure what other's opinions are on doing it this way vs some other way
I'm kind of new to react, so what i wanted was that, I have a toggle button to toggle a persons component and I have a cockpit component. But whenever I toggle the persons component, I don't want to always re-render the cockpit component.
So this is my Cockpit.js component file.
import React, { useEffect } from 'react';
import classes from './Cockpit.css';
const cockpit = props => {
useEffect(() => {
console.log('[Cockpit.js] useEffect');
// Http request...
setTimeout(() => {
alert('Saved data to cloud!');
}, 1000);
return () => {
console.log('[Cockpit.js] cleanup work in useEffect');
};
}, []);
useEffect(() => {
console.log('[Cockpit.js] 2nd useEffect');
return () => {
console.log('[Cockpit.js] cleanup work in 2nd useEffect');
};
});
// useEffect();
const assignedClasses = [];
let btnClass = '';
if (props.showPersons) {
btnClass = classes.Red;
}
if (props.personsLength <= 2) {
assignedClasses.push(classes.red); // classes = ['red']
}
if (props.personsLength <= 1) {
assignedClasses.push(classes.bold); // classes = ['red', 'bold']
}
return (
<div className={classes.Cockpit}>
<h1>{props.title}</h1>
<p className={assignedClasses.join(' ')}>This is really working!</p>
<button className={btnClass} onClick={props.clicked}>
Toggle Persons
</button>
</div>
);
};
export default React.memo(cockpit);
And this is my App.js
import React, { Component } from 'react';
import Persons from '../Components/Persons/Persons';
import classes from './App.css';
import Cockpit from '../Components/Cockpit/Cockpit'
class App extends Component {
constructor(props) {
super(props);
console.log("[App.js] constructor");
}
state = {
persons: [{id: "abc", name: "", age: 45},
{id: "azz", name: "", age: 56},
{id: "asq", name: "", age: 62}],
showPersons: false,
showCockpit: true
}
static getDerivedStateFromProps(props, state) {
console.log("[App.js] getDerivedStateFromProps", props)
return state;
}
componentDidMount() {
console.log('[App.js] componentDidMount')
}
shouldComponentUpdate(nextProps, nextState) {
console.log('[App.js] shouldCompoentUpdate');
return true;
}
componentDidUpdate() {
console.log('[App.js] componentDidUpdate')
}
deletePersonHandler = (i) => {
const persons = [...this.state.persons];
persons.splice(i, 1);
this.setState({persons: persons})
}
switchNameHandler = (newName) => {
this.setState({persons: [{name: newName, age: 50}, {name: "Aysha", age: 56}, {name: "Momma", age: 62}]})
}
nameSwitchHandler = (event, id) => {
const personIndex = this.state.persons.findIndex(p => {
return p.id === id;
})
const person = {...this.state.persons[personIndex]}
person.name = event.target.value;
const persons = [...this.state.persons]
persons[personIndex] = person;
this.setState({persons: persons})
}
togglePersonHandler = () => {
let doesChange = this.state.showPersons;
this.setState({showPersons: !doesChange})
}
render() {
console.log("[App.js] render");
let person = null;
if(this.state.showPersons) {
person = (<Persons
persons={this.state.persons}
clicked={this.deletePersonHandler}
changed={this.nameSwitchHandler} />
);
}
return (
<div className={classes.App}>
<button onClick={() => this.setState({showCockpit: false})}>Remove Cockpit</button>
{this.state.showCockpit ? (<Cockpit
title={this.props.appTitle}
showPersons={this.state.showPersons}
personsLength={this.state.persons.length}
clicked={this.togglePersonHandler} />) : null}
{person}
</div>
);
}
}
export default App;
But even when I toggle it, useEffect in cockpit component still console logs in the browser console when its not supposed to. I can't seem to find what I am doing wrong.
As you can see in this image the useEffect component in cockpit still renders in the console......
Browser Console
React.memo will do a shallow equal comparison on the props object by default. That means it will check every top level item in the props for equality and if any of them changed it will re-render.
When you click your persons toggle button it will change showPersons in your App component wich is also a prop that you pass to <Cockpit>. Therefore it will re-render even with React.memo. If it wouldn't re-render it wouldn't correctly update your Button class adding or removing classes.Red because this is dependent on the showPersons prop.
It has nothing to do with your useEffect inside of cockpit which will only get called after it re-renders but doesn't cause it to re-render in the first place.
On the click of Toggle Persons, you are changing the state in App Component.
This results in the re-rendering of the App and Cockpit components.
useEffect(() => {
console.log('[Cockpit.js] 2nd useEffect');
return () => {
console.log('[Cockpit.js] cleanup work in 2nd useEffect');
};
});
The above code will trigger every render as you haven't provided dependency.
To fix this, you need to add a dependency to the above code.
Since showPersons change it detects it as changed props.
You can add an equality function in React.memo that tells react when to consider the memoization stale:
// Will only rerender when someValue changes
export default React.memo(Cockpit, (oldProps, newProps) => oldProps.someValue === newProps.someValue)
I am new to react and I am trying to make a POST request using text field data, can anyone help me with how to store that input and make a request after a button is pressed.
I attempted to use useRef() which allowed me to obtain the data however I was not able to store it as a data object to then persist.
Currently my data persists, however it persists an empty object and the state is not being updated.
If anyone can help, I will really appreciate that.
Below is my App.js class
import React, { useState, useEffect, useRef, Component } from 'react';
import axios from "axios";
const api = axios.create({
baseURL: "http://localhost:8080/artists"
});
class App extends Component {
state = {
artists: [],
theArtistName: ""
}
constructor(props){
super(props);
this.getArtists()
}
//calling this method will allow artist array to be populated everytime an event occurs, e.g POST, PUT, DELETE
getArtists = async () =>{
let data = await api.get("/").then(({ data }) => data);
this.setState({artists: data}) //setting our artists to be the data we fetch
}
createArtist = async () =>{
let response = await api.post('/', {name: this.state.theArtistName})
console.log(response)
this.getArtists()
}
deleteArtist = async (id) =>{
let data = await api.delete('/${id}')
this.getArtists();
}
handleAddArtist = (event) =>{
event.preventDefault()
this.setState({
theArtistName: event.target.value
})
const data = this.state.theArtistName
console.log(data)
}
componentDidMount(){
this.createArtist()
}
render(){
// const {theArtistName} = this.state
return(
<>
<input type={Text} placeholder="Enter Artist Name" name="theArtistName"></input>
<button onClick={this.createArtist}>Add Artist</button>
{this.state.artists.map(artist => <h4 key={artist.id}>{artist.name}
<button onClick={() =>this.deleteArtist(artist.id)}>Delete artist</button></h4>)}
</>
)
}
}
export default App;
this.setState is an async function, it takes second argument as callback. This should solve your problem. i.e.
import React, { useState, useEffect, useRef, Component } from "react";
import axios from "axios";
const api = axios.create({
baseURL: "http://localhost:8080/artists",
});
class App extends Component {
constructor(props) {
super(props);
this.state = {
artists: [],
theArtistName: "",
};
}
//calling this method will allow artist array to be populated everytime an event occurs, e.g POST, PUT, DELETE
getArtists = async () => {
let data = await api.get("/").then(({ data }) => data);
this.setState({ artists: data }); //setting our artists to be the data we fetch
};
createArtist = async () => {
let response = await api.post("/", { name: this.state.theArtistName });
console.log(response);
this.getArtists();
};
deleteArtist = async (id) => {
let data = await api.delete("/${id}");
this.getArtists();
};
handleAddArtist = (event) => {
event.preventDefault();
this.setState(
{
theArtistName: event.target.value,
},
() => {
this.createArtist();
}
);
};
componentDidMount() {
this.getArtists();
}
render() {
// const {theArtistName} = this.state
return (
<>
<input
type={Text}
placeholder="Enter Artist Name"
name="theArtistName"
></input>
<button onClick={this.handleAddArtist}>Add Artist</button>
{this.state.artists.map((artist) => (
<h4 key={artist.id}>
{artist.name}
<button onClick={() => this.deleteArtist(artist.id)}>
Delete artist
</button>
</h4>
))}
</>
);
}
}
export default App;
Let me know if it helps.
because react update state asynchronously so when you are invoking handleAddArtist function which update state the event might be gone so you need to store the value from the event in variable like this :
handleAddArtist = (event) =>{
event.preventDefault()
const {value} = e.target
this.setState({
theArtistName: value
})
}
and to check state update there is a lifecycle method called componentDidUpdate for class component and useEffect for functional component.
[edit]:
call this.createArtist() in componentDidUpdate like this :
componentDidUpdate(prevProps,prevState){
if(prevState.theArtistName!==this.state.theArtistName)
this.createArtist()
}
so the createArtist will fire only when theArtistName state change.
First of all, useRef is a hook only meant for function components and not for class components. For using Refs in class components use React.createRef().
Usually, HTML input elements maintain their own state. The usual way to access the value of an input element from a React component that renders it is to control the input element's state via this component by adding an onChange listener and a value attribute to the input element:
class App extends Component{
constructor(props) {
super(props);
this.state = {artistName: ""};
this.handleArtistNameChange = this.handleArtistNameChange.bind(this);
}
handleArtistNameChange(event) {
this.setState({artistName: event.target.value});
}
render(){
return (
<input
type="text"
value={this.state.artistName}
onChange={this.handleArtistNameChange}
/>
);
}
}
Whenever the value of the input element changes the App component will rerender with the most up-to-date value of the input in its state.
Here is a working example:
You can read more on using form elements in React here.
new user to Redux so apologies for any silly mistakes. Ultimately, I am trying to toggle a className in component B as an onClick function toggles a state in component A.
So it should be:
Component A button click => state toggles => Component B className toggles.
I have setup my store and my mapDispatchToProps function that changes the state in my store seems to be working. However, calling this state in a different component is working... but, does not re-render as the state in the store toggles. There is more content in my code, but I've tried to strip the parts that are not necessary to this issue.
Component A - containing a button that toggles a state and changes a state within my store/rootReducer:
import React from 'react';
import { connect } from 'react-redux';
function Nav(props) {
const [showMenu, setShowMenu] = React.useState('menuClosed');
function menuClick() {
showMenu === 'menuClosed' ? setShowMenu('menuOpen') :
setShowMenu('menuClosed');
}
// this works fine, I know this due to the console.log on my rootReducer page:
React.useEffect(() => {
props.toggleMenu(showMenu);
}, [showMenu])
return (
<button className="hvr-icon-grow-rotate" id="bars" onClick={() => { menuClick(); }}>
<FontAwesomeIcon icon={faBars} className="hvr-icon" />
</button>
)
}
const mapStateToProps = (state) => {
return {
menu: state.menu
}
}
const mapDispatchToProps = (dispatch) => {
return {
toggleMenu: (showMenu) => {
dispatch({ type: 'TOGGLE_MENU', menu: showMenu })
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Nav)
Component B: supposed to toggle a className depending on store state:
import React from 'react';
import { connect } from 'react-redux';
const [noHover, setNoHover] = React.useState('projectGrid');
React.useEffect(() => {
console.log('portfolio props: ' + props.menu + ' noHover: ' + noHover);
if (props.menu === 'menuOpen') {
setNoHover('projectGrid noHover');
console.log('portfolio props: ' + props.menu + ' noHover: ' + noHover);
}
else if (props.menu === 'menuClosed') {
setNoHover('projectGrid');
console.log('portfolio props: ' + props.menu + ' noHover: ' + noHover);
}
}, [])
return (<div className={noHover}>some content</div>);
}
const mapStateToProps = (state) => {
return {
menu: state.menu
}
}
export default connect(mapStateToProps)(PortfolioItem)
finally, content of my rootReducer.js page, or the Redux store:
const initState = {
menu: ['menuClosed']
}
const rootReducer = (state = initState, action) => {
console.log(action);
return state;
}
export default rootReducer;
Any help would be greatly appreciated - thank you!
It doesn't look like you're actually toggling anything in your reducer.
You need to return a new state if you want it to change. I'm not sure exactly how it should look in your case, but something like this:
const rootReducer = (state = initState, action) => {
let newState = {...state}; // Create a new state object so we don't mutate original
switch(action.type) { // Check what type was sent to see if we should update
case 'TOGGLE_MENU':
newState.menu = action.menu; // Set new state based on action
return newState;
default:
return state; // Return old state if nothing changed
}
}
Side note: it looks like your initial state has menu as an array, but your components don't treat it as one. It seems like it should be defaulted to a string.
It looks like #brian-thompson has solved your problem, so I won't solve it again, but I would say you can make your styling logic a log easier by using the classnames library.
Your useEffect callback would be replaced with
const { menu } = props
const noHover = classNames({
'projectGrid': true,
'noHover': menu === 'menuOpen'
})
Removing the need for any hooks.
I have updated this with an update at the bottom
Is there a way to maintain a monolithic root state (like Redux) with multiple Context API Consumers working on their own part of their Provider value without triggering a re-render on every isolated change?
Having already read through this related question and tried some variations to test out some of the insights provided there, I am still confused about how to avoid re-renders.
Complete code is below and online here: https://codesandbox.io/s/504qzw02nl
The issue is that according to devtools, every component sees an "update" (a re-render), even though SectionB is the only component that sees any render changes and even though b is the only part of the state tree that changes. I've tried this with functional components and with PureComponent and see the same render thrashing.
Because nothing is being passed as props (at the component level) I can't see how to detect or prevent this. In this case, I am passing the entire app state into the provider, but I've also tried passing in fragments of the state tree and see the same problem. Clearly, I am doing something very wrong.
import React, { Component, createContext } from 'react';
const defaultState = {
a: { x: 1, y: 2, z: 3 },
b: { x: 4, y: 5, z: 6 },
incrementBX: () => { }
};
let Context = createContext(defaultState);
class App extends Component {
constructor(...args) {
super(...args);
this.state = {
...defaultState,
incrementBX: this.incrementBX.bind(this)
}
}
incrementBX() {
let { b } = this.state;
let newB = { ...b, x: b.x + 1 };
this.setState({ b: newB });
}
render() {
return (
<Context.Provider value={this.state}>
<SectionA />
<SectionB />
<SectionC />
</Context.Provider>
);
}
}
export default App;
class SectionA extends Component {
render() {
return (<Context.Consumer>{
({ a }) => <div>{a.x}</div>
}</Context.Consumer>);
}
}
class SectionB extends Component {
render() {
return (<Context.Consumer>{
({ b }) => <div>{b.x}</div>
}</Context.Consumer>);
}
}
class SectionC extends Component {
render() {
return (<Context.Consumer>{
({ incrementBX }) => <button onClick={incrementBX}>Increment a x</button>
}</Context.Consumer>);
}
}
Edit: I understand that there may be a bug in the way react-devtools detects or displays re-renders. I've expanded on my code above in a way that displays the problem. I now cannot tell if what I am doing is actually causing re-renders or not. Based on what I've read from Dan Abramov, I think I'm using Provider and Consumer correctly, but I cannot definitively tell if that's true. I welcome any insights.
There are some ways to avoid re-renders, also make your state management "redux-like". I will show you how I've been doing, it far from being a redux, because redux offer so many functionalities that aren't so trivial to implement, like the ability to dispatch actions to any reducer from any actions or the combineReducers and so many others.
Create your reducer
export const initialState = {
...
};
export const reducer = (state, action) => {
...
};
Create your ContextProvider component
export const AppContext = React.createContext({someDefaultValue})
export function ContextProvider(props) {
const [state, dispatch] = useReducer(reducer, initialState)
const context = {
someValue: state.someValue,
someOtherValue: state.someOtherValue,
setSomeValue: input => dispatch('something'),
}
return (
<AppContext.Provider value={context}>
{props.children}
</AppContext.Provider>
);
}
Use your ContextProvider at top level of your App, or where you want it
function App(props) {
...
return(
<AppContext>
...
</AppContext>
)
}
Write components as pure functional component
This way they will only re-render when those specific dependencies update with new values
const MyComponent = React.memo(({
somePropFromContext,
setSomePropFromContext,
otherPropFromContext,
someRegularPropNotFromContext,
}) => {
... // regular component logic
return(
... // regular component return
)
});
Have a function to select props from context (like redux map...)
function select(){
const { someValue, otherValue, setSomeValue } = useContext(AppContext);
return {
somePropFromContext: someValue,
setSomePropFromContext: setSomeValue,
otherPropFromContext: otherValue,
}
}
Write a connectToContext HOC
function connectToContext(WrappedComponent, select){
return function(props){
const selectors = select();
return <WrappedComponent {...selectors} {...props}/>
}
}
Put it all together
import connectToContext from ...
import AppContext from ...
const MyComponent = React.memo(...
...
)
function select(){
...
}
export default connectToContext(MyComponent, select)
Usage
<MyComponent someRegularPropNotFromContext={something} />
//inside MyComponent:
...
<button onClick={input => setSomeValueFromContext(input)}>...
...
Demo that I did on other StackOverflow question
Demo on codesandbox
The re-render avoided
MyComponent will re-render only if the specifics props from context updates with a new value, else it will stay there.
The code inside select will run every time any value from context updates, but it does nothing and is cheap.
Other solutions
I suggest check this out Preventing rerenders with React.memo and useContext hook.
I made a proof of concept on how to benefit from React.Context, but avoid re-rendering children that consume the context object. The solution makes use of React.useRef and CustomEvent. Whenever you change count or lang, only the component consuming the specific proprety gets updated.
Check it out below, or try the CodeSandbox
index.tsx
import * as React from 'react'
import {render} from 'react-dom'
import {CountProvider, useDispatch, useState} from './count-context'
function useConsume(prop: 'lang' | 'count') {
const contextState = useState()
const [state, setState] = React.useState(contextState[prop])
const listener = (e: CustomEvent) => {
if (e.detail && prop in e.detail) {
setState(e.detail[prop])
}
}
React.useEffect(() => {
document.addEventListener('update', listener)
return () => {
document.removeEventListener('update', listener)
}
}, [state])
return state
}
function CountDisplay() {
const count = useConsume('count')
console.log('CountDisplay()', count)
return (
<div>
{`The current count is ${count}`}
<br />
</div>
)
}
function LangDisplay() {
const lang = useConsume('lang')
console.log('LangDisplay()', lang)
return <div>{`The lang count is ${lang}`}</div>
}
function Counter() {
const dispatch = useDispatch()
return (
<button onClick={() => dispatch({type: 'increment'})}>
Increment count
</button>
)
}
function ChangeLang() {
const dispatch = useDispatch()
return <button onClick={() => dispatch({type: 'switch'})}>Switch</button>
}
function App() {
return (
<CountProvider>
<CountDisplay />
<LangDisplay />
<Counter />
<ChangeLang />
</CountProvider>
)
}
const rootElement = document.getElementById('root')
render(<App />, rootElement)
count-context.tsx
import * as React from 'react'
type Action = {type: 'increment'} | {type: 'decrement'} | {type: 'switch'}
type Dispatch = (action: Action) => void
type State = {count: number; lang: string}
type CountProviderProps = {children: React.ReactNode}
const CountStateContext = React.createContext<State | undefined>(undefined)
const CountDispatchContext = React.createContext<Dispatch | undefined>(
undefined,
)
function countReducer(state: State, action: Action) {
switch (action.type) {
case 'increment': {
return {...state, count: state.count + 1}
}
case 'switch': {
return {...state, lang: state.lang === 'en' ? 'ro' : 'en'}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function CountProvider({children}: CountProviderProps) {
const [state, dispatch] = React.useReducer(countReducer, {
count: 0,
lang: 'en',
})
const stateRef = React.useRef(state)
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {count: state.count},
})
document.dispatchEvent(customEvent)
}, [state.count])
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {lang: state.lang},
})
document.dispatchEvent(customEvent)
}, [state.lang])
return (
<CountStateContext.Provider value={stateRef.current}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
)
}
function useState() {
const context = React.useContext(CountStateContext)
if (context === undefined) {
throw new Error('useCount must be used within a CountProvider')
}
return context
}
function useDispatch() {
const context = React.useContext(CountDispatchContext)
if (context === undefined) {
throw new Error('useDispatch must be used within a AccountProvider')
}
return context
}
export {CountProvider, useState, useDispatch}
To my understanding, the context API is not meant to avoid re-render but is more like Redux. If you wish to avoid re-render, perhaps looks into PureComponent or lifecycle hook shouldComponentUpdate.
Here is a great link to improve performance, you can apply the same to the context API too