How to make JavaScript function runs only when parameter changes - javascript

I am using React and every time something changes it renders the whole component. Although I have a smallest possible component, but it runs a heavy function. I don't want this function to run on every render if its parameters has not changed. Something like React.memo (basically don't re-render a component if its props have not changed). Is there an equivalent of React.memo for static JavaScript functions?
NOTE: I don't want to pull in a library such as reselect. There has to be a better way!
Edit: I feel like I was not very clear about what I was looking for. Let me ask this with an example,
https://codesandbox.io/s/react-typescript-2xe2m?expanddevtools=1&fontsize=14&hidenavigation=1&theme=dark
Every time I click on + or -, it runs the pleaseMemoizeThisFunction function, even though its parameter has not changed. How can I have this function only runs when any of its parameters change.

Use the useMemo hook around your functions and it will not run unless the params have changed.
https://reactjs.org/docs/hooks-reference.html#usememo
const computed = useMemo(() => calculateExpensive(param1, param2), [param1, param2]);

This is a simple memoization implementation that checks wether the function is called with the same arguments as the last time, and only recalculates the result if they differ.
Seing in your sandbox that you use Typescript, I've added the Types to this function.
function memo<A extends any[], R>(fn:(...args:A) => R) {
let value:R,
before: A = {length:NaN} as any;
const sameAsBefore = (v:A[number], i:number) => v === before[i];
function memoized (...args:A):R {
if (args.length !== before.length || !args.every(sameAsBefore)) {
before = args;
value = fn.apply(this, args);
}
return value;
}
}
usage:
const pleaseMemoizeThisFunction = memo((a: string, b: string): string => {
console.count("function run: ");
// it will do some heavy stuff here
return `I am done with ${a} and ${b}`;
});
or like this.someMethod = memo(this.someMethod);

Related

Is there a way in React.js to run a side effect after `useState` when the value of the state doesn't change in functional components?

One thing I've recently been confused about is what the best way to run effects after a useState call, when the useState doesn't necessarily change the previous state value. For example, say I have a submit function in a form. Also say I have a state called randomNumber, and say in that submit function I call setRandomNumber and set the random number state to a random number from 1-2. Now say that for some reason every time the randomNumber is set, regardless of whether its value changes or not, we want to update the number in some database and navigate to a different page. If I use a useEffect(() => {updateDatabase(); }, [randomNumber]), the problem is that this will update the database even when the functional component first renders, which I do not want. Also, if the randomNumber doesn't change because we pick the same random number twice, we won't update the database after the second setRandomNumber call and we won't navigate to a different page which I don't want.
What would be the best way to 'await' a useState call. I read some articles saying we should potentially use flushSync, but I tried this and it doesn't seem to be working and it seems like this is a very unstable solution as of now.
Thanks!!!
You have 2 different requests here.
How to skip a useEffect on the first render. For this you could employ useRef as this does not trigger a re-render
const Comp = () => {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return
} else {
// do your stuff
}
}, [randomNumber])
}
How to trigger useEffect even if you get the same number. Probably the simplest way would be to wrap your number inside an object and always generate a new object when generating a new number. This way you'll get a new ref all the time and the useEffect will always trigger. If you already have an object you could create a copy using spread operator. Something like this:
const Comp = () => {
const [randomNumber, setRandomNumber] = useState({number: 0})
useEffect(() => {
console.log(randomNumber.number)
}, [randomNumber])
// generate a new random number and set it like this
setRandomNumber({number: Math.random() + 1))
}

useSelector does not work when I set two state inside?

Why doesn't my code work? I am trying to separate these two things into one, but useSelector does not work.
This code works
const st = useSelector((state) =>
state.or.st
);
const ter = useSelector(
({ main }) => main.st.ter
);
This code does not work
I want to put these two things in one useSelector and not in two?
Issue 1: Incorrect Function Usage
The useSelector hook expects a function that takes a single parameter state.
state is the whole state held by redux.
This would mean that your combine function should look more like the following:
const { units, organistationTerminology } = useSelector(state => {
organistationTerminology = state.main.organisation.terminology;
units = state.organisationStructure.units.sort(
(a, b) => a.sortOrder - b.sortOrder
);
return {
organistationTerminology,
units
}
});
Issue 2: Changing Reference
The combined selector function returns a new object which will have a difference reference each time the selector function is computed.
The default comparison used by useSelector to detect a change between values is a reference comparison.
This will result in your component being rerendered on any store change regardless of whether state.main.organisation.terminology or state.organisationStructure.units is updated.
Issue 3: Computation in Selector
Selectors will run on each store change and the resulting value will be compared with the previous value to determine whether a component rerender needs to be triggered.
This means that even if state.organisationStructure.units never changes you will still be running a sort on every store change.
Computations should, where possible, be preformed outside of the selector function.
Issue 4: Sort on State Value
sort() sorts the array in place which would be mutating your state.
Conclusion
Selector functions should be kept as simple / quick as possible as they will be run on every store change.
Pulling multiple values from separate sections of state in one selector usually adds more complexity than it is worth.
My suggestion would be to keep it simple:
const units = useSelector(state => state.organisationStructure.units);
const organistationTerminology = useSelector(state => state.main.organisation.terminology);
const sortedUnits = [...units].sort();
// OR the sort can be memoized using units as a dependency;
const sortedUnits = useMemo(() => [...units].sort(), [units]);
If this is something you will be reusing in a number of components you could wrap it up in a custom hook:
const useTerminologyAndSortedUnits = () => {
const units = useSelector(state => state.organisationStructure.units);
const terminology = useSelector(state => state.main.organisation.terminology);
const sortedUnits = useMemo(() => [...units].sort(), [units]);
return {
units: sortedUnits,
terminology
}
};
// The following can then be used in any of your function components
const { units, terminology } = useTerminologyAndSortedUnits()

React functional component: Trying to set a state once a window break point is reached

Here is my code for the problem, my issue is that the event is triggering multiple times when it hits the 960px breakpoint. This should only fire once it reaches the breakpoint, but I am getting an almost exponential amount of event triggers.
const mediaQuery = '(max-width: 960px)';
const mediaQueryList = window.matchMedia(mediaQuery);
mediaQueryList.addEventListener('change', (event) => {
if (event.matches) {
setState((prevState) => ({
...prevState,
desktopNavActivated: false,
}));
} else {
setState((prevState) => ({
...prevState,
menuActivated: false,
navItemExpanded: false,
}));
}
});```
Hi there and welcome to StackOverflow!
I'm going to assume, that your code does not run within a useEffect hook, which will result in a new event listener being added to your mediaQueryList every time your component gets updated/rendered. This would explain the exponential amount of triggers. The useEffect hook can be quite unintuitive at first, so I recommend reading up on it a bit. The docs do quite a good job at explaining the concept. I also found this article by Dan Abramov immensely helpful when I first started using effects.
The way you call your setState function will always cause your component to update, no matter whether the current state already matches your media query, because you pass it a new object with every update. Unlike the setState method of class based components (which compares object key/values iirc), the hook version only checks for strict equality when determining whether an update should trigger a re-render. An example:
{ foo: 'bar' } === { foo: 'bar' } // Always returns false. Try it in your console.
To prevent that from happening, you could set up a state hook that really only tracks the match result and derive your flags from it. Booleans work just fine with reference equality:
const [doesMatch, setDoesMatch] = useState(false)
So to put it all together:
const mediaQuery = '(max-width: 960px)';
const mediaQueryList = window.matchMedia(mediaQuery);
const MyComponent = () =>
const [match, updateMatch] = useState(false)
useEffect(() => {
const handleChange = (event) => {
updateMatch(event.matches)
}
mediaQueryList.addEventListener('change', handleChange)
return () => {
// This is called the cleanup phase aka beforeUnmount
mediaQueryList.removeEventListener('change', handleChange)
}
}, []) // Only do this once, aka hook-ish way of saying didMount
const desktopNavActivated = !match
// ...
// Further process your match result and return JSX or whatever
}
Again, I would really advise you to go over the React docs and some articles when you find the time. Hooks are awesome, but without understanding the underlying concepts they can become very frustrating to work with rather quickly.

how to use react memo

I am trying to make a simple task manager app and I want to implement react memo in TaskRow (task item) but when I click the checkbox to finish the task, the component properties are the same and I cannot compare them and all tasks are re-rendered again, any suggestions? Thanks
Sand Box: https://codesandbox.io/s/interesting-tharp-ziwe3?file=/src/components/Tasks/Tasks.jsx
Tasks Component
import React, { useState, useEffect, useCallback } from 'react'
import TaskRow from "../TaskRow";
function Tasks(props) {
const [taskItems, setTaskItems] = useState([])
useEffect(() => {
setTaskItems(JSON.parse(localStorage.getItem('tasks')) || [])
}, [])
useEffect(() => {
if (!props.newTask) return
newTask({ id: taskItems.length + 1, ...props.newTask })
}, [props.newTask])
const newTask = (task) => {
updateItems([...taskItems, task])
}
const toggleDoneTask = useCallback((id) => {
const taskItemsCopy = [...taskItems]
taskItemsCopy.map((t)=>{
if(t.id === id){
t.done = !t.done
return t
}
return t
})
console.log(taskItemsCopy)
console.log(taskItems)
updateItems(taskItemsCopy)
}, [taskItems])
const updateItems = (tasks) => {
setTaskItems(tasks)
localStorage.setItem('tasks', JSON.stringify(tasks))
}
return (
<React.Fragment>
<h1>learning react </h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Done</th>
</tr>
</thead>
<tbody>
{
props.show ? taskItems.map((task, i) =>
<TaskRow
task={task}
key={task.id}
toggleDoneTask={()=>toggleDoneTask(task.id)}>
</TaskRow>)
:
taskItems.filter((task) => !task.done)
.map((task) =>
<TaskRow
show={props.show}
task={task}
key={task.id}
toggleDoneTask={()=>toggleDoneTask(task.id)}></TaskRow>
)
}
</tbody>
</table>
</React.Fragment>
)
}
export default Tasks
Item task (TaskRow component)
import React, { memo } from 'react'
function TaskRow(props) {
return (<React.Fragment>
{console.log('render', props.task)}
<Tr show={props.show} taskDone={props.task.done}>
<td>
{props.task.title}
</td>
<td>
{props.task.description}
</td>
<td>
<input type="checkbox"
checked={props.task.done}
onChange={props.toggleDoneTask}
/>
</td>
</Tr>
</React.Fragment>)
}
export default memo(TaskRow, (prev,next)=>{
console.log('prev props', prev.task)
console.log('next props', next.task)
})
I'm afraid there are quite a lot of problems with the code you've shared, only some of them due to React.memo. I'll start there and work through the ones I've spotted.
You've provided an equality testing function to memo, but you are not using it to test anything. The default behaviour, which requires no testing function, will shallowly compare props between the previous and the next render. This means it will pick up on differences between primitive values (e.g. string, number, boolean) and references to objects (e.g. literals, arrays, functions), but it will not automatically deeply compare those objects.
Remember, memo will only allow rerenders when the equality testing function returns false. You've provided no return value to the testing function, meaning it returns undefined, which is falsy. I've provided a simple testing function for an object literal with primitive values which will do the job needed here. If you have more complex objects to pass in the future, I suggest using a comprehensive deep equality checker like the one provided by the lodash library, or, even better, do not pass objects at all if you can help it and instead try to stick to primitive values.
export default memo(TaskRow, (prev, next) => {
const prevTaskKeys = Object.keys(prev.task);
const nextTaskKeys = Object.keys(next.task);
const sameLength = prevTaskKeys.length === nextTaskKeys.length;
const sameEntries = prevTaskKeys.every(key => {
return nextTaskKeys.includes(key) && prev.task[key] === next.task[key];
});
return sameLength && sameEntries;
});
While this solves the initial memoisation issue, the code is still broken for a couple of reasons. The first is that despite copying your taskItems in toggleTaskDone, for similar reasons to those outlined above, your array of objects is not deeply copied. You are placing the objects in a new array, but the references to those objects are preserved from the previous array. Any changes you make to those objects will be directly mutating the React state, causing the values to become out of sync with the rest of your effects.
You can solve this by mapping the copy and spreading the objects. You would have to do this for every level of object reference in any state object you attempt to change, which is part of the reason that React advises against complex objects in useState (one level of depth is usually fine).
const taskItemsCopy = [...taskItems].map((task) => ({ ...task }));
Side Note: You are not doing anything with the result of taskItemsCopy in your original code. map is not a mutating method - calling it without assigning the result to a variable does nothing.
The next issue is more subtle, and demonstrates one of the pitfalls and potential complications when memoising your components. The toggleTaskDone callback has taskItems in its dependency array. However, you are passing it as a prop in an anonymous function to TaskRow. This prop is not being considered by React.memo - we're specifically ignoring it because we only want to rerender on changes to the task object itself. This means that when a task does change its done status, all the other tasks are becoming out of sync with the new value of taskItems - when they change their done status, they will be using the value of taskItems as it was the last time they were rendered.
Inline anonymous functions are recreated on every render, so they are always unequal by reference. You could actually fix this somewhat by adjusting the way the callback is passed and executed:
// Tasks.jsx
toggleDoneTask={toggleDoneTask}
// TaskRow.jsx
onChange={() => props.toggleDoneTask(props.task.id)}
In this way you would be able to check for reference changes in your memo equality function, but since the callback changes every time taskItems changes, this would make the memoisation completely useless!
So, what to do. This is where the implementation of the rest of the Tasks component starts to limit us a bit. We can't have taskItems in the dependency of toggleTaskDone, and we also can't call updateItems because that has the same (implicit) dependency. I've provided a solution which technically works, although I would consider this a hack and not really recommended for actual usage. It relies on the callback version of setState which will allow us to have access to the current value of taskItems without including it as a dependency.
const toggleDoneTask = useCallback((id) => {
setTaskItems((prevItems) => {
const prevCopy = [...prevItems].map((task) => ({ ...task }));
const newItems = prevCopy.map((t) => {
if (t.id === id) t.done = !t.done;
return t;
});
localStorage.setItem("tasks", JSON.stringify(newItems));
return newItems;
});
}, []);
Now it doesn't matter that we aren't equality checking the handler prop, because the function never alters from the initial render of the component. With these changes implemented my fork of your sandbox seems to be working as expected.
On a broader note, I really think you should consider writing React code using create-react-app when you're learning the framework. I was a little surprised to see you had a custom webpack set up, and you don't seem to have proper linting for React (bundled automatically in CRA) which would highlight a lot of these issues for you as warnings. Specifically, the misuse of the dependency array in a number of places in the Task component which is going to make it unstable and error prone even with the essential fixes I've suggested.

Refactor multiple useEffects

I need to fetch multiple items (about 6)with different ID's passed via props, names are made up, f.e. headerId, bodyId, footerId.
I made an useEffect for each one:
useEffect(() => {
const getHeader = async () => {
const header = await api.fetch(props.headerId);
setHeader(header);
}
getHeader();
}, [props.headerId])
What I don't like is that now I have same useEffects just with different prop, could I somehow merge all of them or this is the way I should do it.
Passing multiple properties into array within useEffect, like:
}, [props.headerId, props.bodyId]);
will call the function if even one of the passed properties have changed. I believe you don't really want to call every async request to API (for new header, new body and so on) even if only one prop has changed.
Using multiple useEffect allows you to call only that particular request, that it's prop has changed.
You can make a higher order function that can be called with the state setter function and the prop name, that gets passed to useEffect:
const setAPIState = () => (prop, setter) => {
api.fetch(props).then(setter);
};
// Then replace your original code with
const propsAndSetters = [
[props.headerId, setHeader],
[props.bodyId, setBody],
// ...
];
for (const [prop, setter] of propsAndSetters) {
useEffect(setAPIState(prop, setter), prop);
}
It's still somewhat repetitive, but since you want separate API calls for each different prop, you need a different useEffect for each one.

Categories