Repeat re-rendering causing multiple datalayer pushes - javascript

One of the journeys in my app kicks off several consecutive dispatches, one main dispatch with several side effects based on the result of the preceding one - with each API call making a Redux state change via the reducer.
After the API calls I am feeding data to a separate microservice to bring back logic that will dictate the sub-component to render. And it is in these sub-components that I am wanting to make a single datalayer push.
The issue I am having is that I am getting multiple renders/rerenders due to the constant data changes each time the reducer is hit, as you would imagine... And each time the main parent component is rendered/rerendered due to state change, I am sending a datalayer push as the sub-component is rendered again...
I'm wondering if there are any ways in which I can stop the rendering so much and the constant triggering of my sub-component and its datalayer push.
Note - I have tried wrapping these components with React.memo and using a custom prop checker using lodash.isEqual, however the Redux state changes slightly after each reducer call, so this doesn't really help.
SubComponent.jsx
const SubComponent = props => {
useEffect(() => {
// Do datalayer push here
}, []); // useEffect runs once on render
// Return html here
}
MyComponent.jsx
const mapStateToProps = state => ({...});
const mapDispatchToProps = dispatch => ({...});
const MyComponent = (props) => {
// Note: Runs on each render - Will be required to run when any redux state changes
useEffect(() => {
// Set up microservice wizard config
});
return (
<div>
{microserviceWizard.renderPage(props.step)}
<EcommerceHandler /> // This also makes Redux state changes
</div>
);
}

Related

Alternative to extending a functional component

I have the following component where within the useEffect, I am calling some data reading related
functions meant to happen once on load.
The problem is, some of the prop data are not available at this stage (still undefined) like the prodData and index.
They are only available when I get into the Nested components like <NestedComponent1 />.
I wish to move this logic into the nested components which will resolve this issue.
But I do not want to repeat these code inside the useEffect for each component. Instead looking to write these 7 lines once maybe in a function
and just call it with the 3 NestedComponents.
Issue is that there is a higher order function wrapping here plus all the values like prodData and index is coming from Redux store.
I can't just move all these logic inside useEffect into a normal JS function and instead need a functional component for this.
And if I make a functional component to perform these operations, I can't call it in the useEffect for each of the NestedComponents.
Cos this is not valid syntax.
React.useEffect(() => {
<NewlyCreatedComponentWithReadingFunctionality />
}, []);
Thus my query is, is there a way I could write a functional component which has the data reading logic inside its useEffect.
And then extend this functional component for each of the functional components so that the useEffect would just fire
when each of these NestedComponents are called?
Doesn't seem to be possible to do this thus looking for alternatives.
This is the existing component where some of these prop values are undefined at this stage.
const MyComponent = ({
prodData,
index,
country,
highOrder: {
AHigherOrderComponent,
},
}) => {
// this is the logic which I am looking to write once and be
// repeatable for all the NestedComponent{1,2,3}s below.
React.useEffect(() => {
const [, code] = country.split('-');
const sampleData = prodData[index].sampleData = sampleData;
const period = prodData[index].period = period;
const indication = prodData[index].indication = indication;
AHigherOrderComponent(someReadDataFunction(code, sampleData));
AHigherOrderComponent(someReadDataFunction(code, period);
AHigherOrderComponent(someReadDataFunction(code, indication);
}, []);
return (
{/* other logics not relevant */}
<div>
<div>
<NestedComponent1 />
<NestedComponent2 />
<NestedComponent3 />
</div>
</div>
);
};
export default connect( // redux connect
({
country,
prodData,
index,
}) => ({
country,
prodData,
index,
})
)(withHighOrder(MyComponent));
React components implement a pattern called composition. There are a few ways to share state between parts of your React application but whenever you have to remember some global state and offer some shared functionality, I would try and manage that logic inside a context provider.
I would try the following:
Wrap all your mentioned components inside a context provider component
Offer the someReadDataFunction as a callback function as part of the context
Within your provider, manage react state, e.g. functionHasBeenCalled that remembers if someReadDataFunction has been called already
Set functionHasBeenCalled to true inside someReadDataFunction
Call someReadDataFunction inside your components within a useEffect based on the props data
This way, your application globally remembers if the function has been executed already but you can still use the latest data within your useEffect within your components to call someReadDataFunction.

React component render twice using useState

I'm having a really hard time to figure out what's happening when there is nothing being used to trigger re-render the component.
Events.js Component renders twice when I remove the useState() from the Event.js it renders once, but I need to keep it. when I use useEffect() inside Event components, renders fourth time.
I just kept the dummy data to give you to fill the emptiness and tried to remove React.memo, nothing happens. the problem is with the Event.js component I believe. I'm also using the Context API, but forth time rendering is too much.
useEffect inside App.js is getting some value from the localStorage, I can't access that direct 'cause the value is undefined by default
sandbox code here: https://codesandbox.io/s/event-manager-reactjs-nbz8z?file=/src/Pages/Events/Events.js
The Events.js file is located on /Pages/Events/Events.js
example code is below
Event.js ( child component )
function Events() {
// Sate Managing
const [allEvents, setAllEvents] = React.useState(null);
console.log('Rendering EventsJs', allEvents);
React.useEffect(() => {
setAllEvents(['apple', 'banana']);
}, []);
return (
<div className="events">
{ console.log('Event Rendered.js =>') }
</div>
)
}
export default React.memo(Events, (prevProps, nextProps) => {
return true;
} );
App.js ( parent component )
import { BrowserRouter, Route, Redirect } from 'react-router-dom';
function App() {
const [userId, setUserId] = React.useState(null);
React.useEffect(() => {
setUserId(1);
}, []);
// Login
return (
<BrowserRouter>
<Navigation />
<Route path='/events' component={Events} />
{console.log('App Rendered')}
</BrowserRouter>
);
}
export default App;
Error:
Your app is working fine. It is rendering as it should. As we know:
A React component re-renders whenever its props or state change.
And react component lifecycle order is:
Initial props/state --> render --> DOM update --> mounted
props/state changed --> render --> DOM update --> updated ... so on
In the example below, it is rendering 2 times and that's correct:
First one (first console.log) is due to initial render with state as []
Second one (second console.log) is due to state change (caused by useEffect) to ['apple', 'banana']
function Events() {
const [allEvents, setAllEvents] = React.useState([]);
console.log('Event Rendered', allEvents);
useEffect(() => {
setAllEvents(['apple', 'banana']);
}, []);
return <>Events</>;
}
About using React.memo:
React.memo only checks for props changes. If your function component wrapped in React.memo has a useState or useContext Hook in its implementation, it will still rerender when state or context change.
You can not skip re-render using React.memo due to change in state. You can only optimize to skip re-rendering caused by change in props.
But in the example above, you don't have props passed from the parent component, the only props passed to Events are those passed by react-router i.e. route props. So, there is no need to use React.memo.
Here is sandbox, check the console.logs. You will see only 3 logs: "App render", "Event render with initial state", "Event render with new state".
EDIT:
If we remove StrictMode from index.html, and add below console.logs in components:
App.js --> console.log('App rendered')
Evenets.js --> console.log('Event rendered', allEvents, isLoading) // (allEvents and isLoading are state variables here)
And go to http://localhost:3000, we see 1 log:
App Rendered
Now click on "Events", we see 3 logs:
1: Event Rendered, [], true
2: Event Rendered, [{}, ... 54 items], true
3: Event Rendered, [{}, ... 54 items], false
which is correct behavior (refer lifecycles order written above):
1st log: render with initial state ([], true)
2nd log: render with new allEvents (54 items) and old isLoading (true)
3rd log: render with old allEvents (54 items) and new isLoading (false)
Below are the right questions to ask now:
Question1:
Why 2nd and 3rd render (log) are separate, should not they be batched (merged) and applied together as they are written in the same function?
fetch('url').then(() => {
// ... code here
setAllEvents([...events])
setLoading(false)
})
Answer:
No, they will not be batched in above code. As explained by Dan Abramov:
This is implementation detail and may change in future versions.
In current release, they will be batched together if you are inside a React event handler. React batches all setStates done during a React event handler, and applies them just before exiting its own browser event handler.
With current version, several setStates outside of event handlers (e.g. in network responses) will not be batched. So you would get two re-renders in that case.
There exists a temporary API to force batching. If you write ReactDOM.unstable_batchedUpdates(() => { this.fn1(); }); then both calls will be batched. But we expect to remove this API in the future and instead batch everything by default.
So, you can write (inside fetch's then), if you want, it will save 1 render:
ReactDOM.unstable_batchedUpdates(() => {
setAllEvents([...events])
setLoading(false)
})
Question2:
What's React event handler in above quote?
Answer: foo in example below. These 2 set states will be batched.
const foo = () => {
setAllEvents([
{ _id: '5ede5af03915bc469a9d598e', title: 'jfklsd', },
])
setLoading(false)
}
<button onClick={foo}>CLICK</button>
Question3:
Does it update HTML DOM as many times as it renders (prints console.log)?
Answer: No. React compares calculated virtual DOMs before updating real DOM, so only those changes are applied to real DOM which are required to update the UI.
Question4:
Why was rendering doubled when we use StrictMode?
Answer: Yes, StrictMode will intentionally double invoke "render" and some other lifecycle methods to detect side-effects. Strict mode checks are run in development mode only; they do not impact the production build.
Well actually this is caused by your usage of React.memo, its second parameter is called areEqual, and you pass in () => false, so you are basically telling React that the props are always changing. Therefore whenever App rerenders, Events rerenders too.
You should let React.memo check for prop changes. By passing () => false you are actually telling that its props always change (they are never equal).
export default React.memo(Events);
Here's a working example.

Fetch graphql query result on render and re-renders BUT LAZILY

I have a React Apollo app and what I am trying to do is that I have a component that renders some data using charts. For this data, I have some filters that I save in the local state of the component (Using hooks)
const [filters, setFilters] = useState(defaultFilters);
Now what I want is that whenever the component mounts, fetch the data using the default filters. But I also want to re-fetch data when the user updates the filters AND CLICKS ON SUBMIT and I'd fetch the results using new filters.
Since I also want to fetch the results on filter update, I am using useLazyQuery hook provided by apollo
const [getData, {data}] = useLazyQuery(GET_DATA_QUERY, { variables: {filters} });
useEffect(getData, []); // this useEffect runs only when the component mounts and never again
But, what happens is whenever my state, filters, updates the getData function is automatically run! ALWAYS! (BEHIND THE SCENE)
How do I handle such cases, where I want to fetch results on mounting and re-rendering.
I have tried using useQuery and refetch provided by it but I get the same problem there, whenever I update the state, the component rerenders and the useQuery hooks is run and makes the call. (That's how I believe it runs)
How do I fix my current code. Calling the getData function inside the useEffect function makes it run on every re-render.
I think I the problem defined in this stackoverflow-question is somewhat similar to mine.
Part of the problem is that you really have two different states that you're trying to utilize a single hook for. You have state that represents your inputs' values in the UI, and then you have state that represents the filters you want to actually apply to your charts. These are two separate bits of state.
The simplest solution is to just do something like this:
const [inputFilters, setInputFilters] = useState(defaultFilters)
const [appliedFilters, setAppliedFilters] = useState(inputFilters)
const { data } = useQuery(GET_DATA_QUERY, { variables: { filters: appliedFilters } })
const handleSubmit = () => setAppliedFilters(inputFilters)
const handleSomeInputChange = event => setInputFilters(...)
This way, you use inputFilters/setInputFilters only to manage your inputs' state. When the user clicks your submit button, the appliedFilters are set to whatever the inputFilters are at the time, and your query will update to reflect the new variables.

redux/react mapping state to props with large amounts of data queuing state updates

Using sockets to listen to the server; the Redux store continually updates with thousands of records of data. The updating of the store only takes a couple of seconds with thousands of objects getting dispatched through actions. However, using the redux connect function to map state to my component with mapStateToProps seems to queue up the changes to the state and updates the state of the component at around 7-10 records per second. This means the React Component takes a really long time to render. Are there any solutions to speed this up? Also, the exact amount of data will always be changing and there is no fixed amount.
Here is my component:
class TestComponent extends Component {
state = {};
componentDidMount() {
this.props.connectToSocket();
}
render() {
const { classes, width, people, vehicles, incidents, locations } = this.props;
return (
<div>
Hello
</div>
);
}
}
TestComponent.propTypes = {
classes: PropTypes.object.isRequired
};
const mapStateToProps = state => {
console.log(state);
return {
people: state.live.people,
vehicles: state.live.vehicles,
incidents: state.live.incidents,
locations: state.live.locations
}
};
const mapDispatchToProps = {
connectToSocket: connectToSocket
};
export default connect(mapStateToProps,mapDispatchToProps(TestComponent));
The action that initialises the socket is executed in the componentDidMount() function. I can then see the state being printed in the console, however, it prints every update with around 7-10 new records a second. With over 5000 updates to the store occurring in a very short time span, mapping the redux store to the props of the component takes a much longer time and it takes over 5 minutes to render the component.
Any ideas?
Generally, the answers here involve some form of batching:
You could batch up the data coming from the socket, so that instead of dispatching N actions with 1 value apiece, you maybe dispatch 5 actions with N/5 values each, or something along that line.
You can use one of the various batching middleware or store enhancers to cut down on the number of Redux subscription notifications.
See the Redux FAQ entry on reducing the number of store update events for further ideas and links.

Update component according to redux

I want to create an app with react and redux. My component subscribed to several states from the redux store, some of the state-data need to be prepared before the rendering can take place. Do I need to put the prepareData function into componentWillReceiveProps and write it to the state afterwards? It seems to create a lot of queries in the componentWillReceiveProps. Is there a best practice?
componentWillReceiveProps(nextProps) {
if (this.props.dataUser !== nextProps.dataUser) {
this.prepareData(nextProps.dataUser);
}
if (this.props.dataProject !== nextProps.dataProject) {
.....
}
if (this.props.dataTasks !== nextProps.dataTasks) {
.....
}
}
As Axnyff suggests, you can do your data preparation in mapStateToProps, this will trigger a render each time your redux state updates (your component can be stateless this way) :
mapStateToProps = (state) => {
const dataUserPrepared = prepareData(state.dataUser);
return { dataUser: dataUserPrepared };
}
If you have a lot of different data to prepare, which updates individually, that can be a loss in performance.
In this case you can use componentWillReceiveProps like in your question, this is fine because the setState in your prepareData() function will be batched with the received props to trigger only one render per prop update.
If you were using an app without redux then the solution would be to prepare your data before you call this.setState().
I believe the same solution applies to when using redux, your can prepare your data inside your action because you return the action object having a type and payload.
You can also prepare your data inside your reducer before returning the state object.
You could even prepare your data inside mapStateToProps of your component.
But in case you want to specific conditions under which component should re-render when state changes, then you do that in shouldComponentUpdate()

Categories