What's the difference between those two patterns ? Is the useMemo usefull ?
Will the number of rerenders change ?
1 - Just rendering a modified prop
const Component = ({ myObject }) => {
const computedValue = changeMyValueWithSomeCalculus(myObject.value)
return (
<div>
{computedValue}
</div>
)
};
2 - Using useMemo
const Component = ({ myObject }) => {
const computedValue = useMemo(() => {
return changeMyValueWithSomeCalculus(myObject.value);
}, [myObject.value]);
return (
<div>
{computedValue}
</div>
)
};
I've ran into this SO question very instructive but doesn't help me out with this choice !
Related
I am creating a custom multiple choice question, but I am having difficulties updating my selection choice using useState.
const QuestionPage = ({ audioFiles }) => {
const [choice, setChoice] = useState(-1); // -1 is when none of the choices are selected
const updateChoice = val => {
setChoice(val);
}
return (
// each MultipleChoice contains an audio file and a radio button
<MultipleChoice audioFiles={audioFiles} choice={choice} updateChoice={updateChoice} />
)
};
const MultipleChoice = ({ audioFiles, choice, updateChoice }) => {
const answerOption = audioFiles.map((item, key) =>
<AudioButton file={file} index={key} choice={choice} updateChoice={updateChoice} />
);
return (
{answerOption}
);
}
const AudioButton = ({ file, index, choice, updateChoice }) => {
const handleClick = (val) => {
updateChoice(val);
};
const radioButton = (
<div className={`${index === choice ? "selected" : ""}`} onClick={() => handleClick(index)}>
</div>
);
return (
<>
{radioButton}
<Audio file={file} />
</>
);
}
In the first function, QuestionPage within updateChoice, when I use console.log(val), it updates according to the selections I make (i.e. 0 and 1). However, when I call console.log(choice), it keeps printing -1.
In addition, I keep getting an error message that says updateChoice is not a function.
Any advice? Thanks in advance!
Looks like you did not pass the value of audioFiles in MultipleChoice function
In Comp1 when i hover mouse on it i want the state to change to true (I'm passing true param to it). Also i want to ensure that by doing so this will not cause the Comp2 component to re-render.
My understanding was that if i do like so useStoreOnHover.setState({ onComp1: true }) it should work but it does not :(
I have also tried with const onComp1Set = useStoreOnHover((s) => s.onComp1Set) but still same :(
The only way i was able to get it working is by const { onComp1Set } = useStoreOnHover() but I'm trying to avoid these type of de-structuring because it also triggers re-renders to other components.
Live example: https://codesandbox.io/s/winter-grass-qxrv8
import create, { GetState, SetState } from "zustand";
type typeStoreOnHover = {
onComp1: boolean;
onComp1Set: (val: boolean) => void;
onComp2: boolean;
};
export const useStoreOnHover = create<typeStoreOnHover>(
(set: SetState<typeStoreOnHover>, get: GetState<typeStoreOnHover>) => {
return {
onComp1: false,
onComp1Set: (val) => set({ onComp1: val }),
onComp2: false
};
}
);
const Comp1 = () => {
const onComp1 = useStoreOnHover.getState().onComp1;
// const onComp1Set = useStoreOnHover((s) => s.onComp1Set);
console.log("Comp 1", onComp1);
return (
<div
onMouseEnter={() => {
// onComp1Set(true);
useStoreOnHover.setState({ onComp1: true });
}}
>
Comp 1 {onComp1 ? "True" : "False"}
</div>
);
};
const Comp2 = () => {
const onComp2 = useStoreOnHover((s) => s.onComp2);
console.log("Comp 2", onComp2);
return <div>Comp 2 </div>;
};
export default function App() {
return (
<>
<Comp1 />
<Comp2 />
</>
);
}
According to zustand documentation, this should be the actual approach,
const onComp1 = useStoreOnHover((s) => s.onComp1);
I have tested it on your CodeSandBox link and it worked.
I am not familiar with the zustand library but this might help you.
Playing with React those days. I know that calling setState in async. But setting an initial value like that :
const [data, setData] = useState(mapData(props.data))
should'nt it be updated directly ?
Bellow a codesandbox to illustrate my current issue and here the code :
import React, { useState } from "react";
const data = [{ id: "LION", label: "Lion" }, { id: "MOUSE", label: "Mouse" }];
const mapData = updatedData => {
const mappedData = {};
updatedData.forEach(element => (mappedData[element.id] = element));
return mappedData;
};
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
export default function App() {
const [loadedData, setLoadedData] = useState(data);
const [filter, setFilter] = useState("");
const filterData = () => {
return loadedData.filter(element =>
filter ? element.id === filter : true
);
};
//loaded comes from a useEffect http call but for easier understanding I removed it
return (
<div className="App">
<button onClick={() => setFilter("LION")}>change filter state</button>
<ChildComponent dataProp={filterData()} />
</div>
);
}
So in my understanding, when I click on the button I call setFilter so App should rerender and so ChildComponent with the new filtered data.
I could see it is re-rendering and mapData(updatedData) returns the correct filtered data BUT ChildComponent keeps the old state data.
Why is that ? Also for some reason it's rerendering two times ?
I know that I could make use of useEffect(() => setMappedData(mapData(dataProp)), [dataProp]) but I would like to understand what's happening here.
EDIT: I simplified a lot the code, but mappedData in ChildComponent must be in the state because it is updated at some point by users actions in my real use case
https://codesandbox.io/s/beautiful-mestorf-kpe8c?file=/src/App.js
The useState hook gets its argument on the very first initialization. So when the function is called again, the hook yields always the original set.
By the way, you do not need a state there:
const ChildComponent = ({ dataProp }) => {
//const [mappedData, setMappedData] = useState(mapData(dataProp));
const mappedData = mapData(dataProp);
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
EDIT: this is a modified version in order to keep the useState you said to need. I don't like this code so much, though! :(
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
let actualMappedData = mappedData;
useMemo(() => {
actualMappedData =mapData(dataProp);
},
[dataProp]
)
console.log("** Render Child Component **");
return Object.values(actualMappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
Your child component is storing the mappedData in state but it never get changed.
you could just use a regular variable instead of using state here:
const ChildComponent = ({ dataProp }) => {
const mappedData = mapData(dataProp);
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
function TypeArticleOne(props) {
let apiData = props.apiData;
const [ therapists, setTherapists ] = useState(apiData.topProfiles.therapists);
const [speciality, setSpeciality]= useState('ABA');
const [pageLoading, setPageLoading]= useState(true);
const topProfilesUrl = 'therapists/top/profiles'
useEffect(() => {
console.log(speciality);
getTopTherapists();
window.scrollTo(0, 0);
}, []);
const getTopTherapists = () => {
setPageLoading(true);
loadTopTherapists();
};
const loadTopTherapists = () => {
console.log("second");
props.actions.reqGetTherapistsTopProfiles({
body: {},
headers: null,
resource: `${topProfilesUrl}`
})
};
useEffect(() => {
if (apiData.topProfiles && apiData.topProfiles.success) {
const therapistsLoad = apiData.topProfiles.therapists;
setPageLoading(false);
setTherapists([therapists].concat(therapistsLoad));
}
}, []);
How to map an array in a functional component? I want to map the therapists array from the functional component above.
I call the therapists in an array from an database and I need to map them to render in a card, inside an functional component.
const renderTherapists = (props) => {
const items = props.therapists.map( (t, idx) => (
<TherapistCard therapist={t} key={idx} />
))
return (
<div ref={0} className="therapist-list">
{ items }
</div>
)
}
EDIT: You probably don't need the React Fragment as you already have a as pointed out in the comments. Possibly the point on destructuring might still help.
ORIGINAL:
You might need to wrap the array in a React Fragment (<>{array}</>). This is required as you cannot directly return an array of components.
Also not sure what the structure of your therapist object is, but if you want to destructure it then it should be ({t, idx}) => instead of (t, idx) => (add the curly braces).
const renderTherapists = (props) => {
const items = props.therapists.map( ({t, idx}) => (
<TherapistCard therapist={t} key={idx} />
))
return (
<div ref={0} className="therapist-list">
<>
{ items }
</>
</div>
)
}
This is for React 16.2.0 and later. See the blog post here: https://reactjs.org/blog/2017/11/28/react-v16.2.0-fragment-support.html
I am looking for opinions on my approach on building React tree view app utilising Hooks.
Here's the code, utilising useCallback, React.memo and useState. Please note that there can be only one 1st level item opened at time, rest of the levels may have multiple items opened at once.
Branch.js:
import React, { useState, useCallback} from 'react'
import Leaf from './Leaf'
const Branch = ({ items }) => {
const [expanded, setExpanded] = useState([])
const clickHandler = useCallback(
({ categoryId, level }) => {
let result
if (level === 1) {
result = expanded.includes(categoryId) ? [] : [categoryId]
} else {
result = expanded.includes(categoryId) ? expanded.filter(item => item !== categoryId) : [ ...new Set([ categoryId, ...expanded])]
}
setExpanded(result)
},[expanded])
return (
<ul>
{items && items.map(item => {
const { categoryId, categoryName, level, eventsCount, children } = item
return (
<Leaf
key={categoryId}
categoryId={categoryId}
name={categoryName}
level={level}
eventsCount={eventsCount}
children={children}
isOpen={expanded.includes(categoryId)}
onClick={clickHandler}
/>
)})}
</ul>
)
}
export default Branch
Leaf.js:
import React from 'react'
import Branch from './Branch'
const Leaf = React.memo(({ name, categoryId, level, children, eventsCount, onClick, isOpen }) => {
const _onClick = () => {
onClick({ categoryId, level })
}
return (
<li className={!isOpen && 'hidden'}>
<button onClick={_onClick}>
<span>{name}</span>
</button>
{children.length ? <Branch items={children}/> : ''}
</li>
)
})
export default Leaf
I'd like someone to review the code for performance (i.e. number of unnecessary re-renders) that might be happening. I am interested in your opinion on my usage of React.memo and click event handler (useCallback).
Does the way I am passing down clickHandler and then receiving and firing that handler causes or prevents additional re-renders?
It would be more efficient with functional updates:
const clickHandler = useCallback(
({ categoryId, level }) => {
setExpanded(expanded => {
let result
if (level === 1) {
result = expanded.includes(categoryId) ? [] : [categoryId]
} else {
result = expanded.includes(categoryId) ? expanded.filter(item => item !== categoryId) : [ ...new Set([ categoryId, ...expanded])]
}
return result
}
}, []
)
So the handler doesn't change at all.
The only major performance limitation in your code is that if expanded changes a new clickHandler callback is created which will cause all Leaf component memoization to break thus re-rendering all components instead of only that particular component whose isOpen prop has changed
So the solution to improve performance involves avoiding recreating clickHandler callback as much as possible. There are two ways to solve the above problems
First: The first solution involves using callback method for setState and using useCallback only on initial render
const Branch = ({ items }) => {
const [expanded, setExpanded] = useState([])
const clickHandler = useCallback(
({ categoryId, level }) => {
setExpanded(prevExpanded => {
let result
if (level === 1) {
result = expanded.includes(categoryId) ? [] : [categoryId]
} else {
result = expanded.includes(categoryId) ? expanded.filter(item => item !== categoryId) : [ ...new Set([ categoryId, ...expanded])]
}
return result;
})
},[])
return (
<ul>
{items && items.map(item => {
const { categoryId, categoryName, level, eventsCount, children } = item
return (
<Leaf
key={categoryId}
categoryId={categoryId}
name={categoryName}
level={level}
eventsCount={eventsCount}
children={children}
isOpen={expanded.includes(categoryId)}
onClick={clickHandler}
/>
)})}
</ul>
)
}
export default Branch;
Second: When the logic to update state becomes complex then using callback method for state update may get confusing and difficult to debug. In such cases its better to make use of useReducer instead of useState and use the dispatch action to set state
const initialState = [];
const reducer = (state, action) => {
switch(action) {
case 'UPDATE_EXPANDED': {
const { level, categoryId } = action;
if (level === 1) {
return state.includes(categoryId) ? [] : [categoryId]
} else {
return state.includes(categoryId) ? state.filter(item => item !== categoryId) : [ ...new Set([ categoryId, ...state])]
}
}
default: return state;
}
}
const Branch = ({ items }) => {
const [expanded, dispatch] = useReducer(reducer, initialState);
return (
<ul>
{items && items.map(item => {
const { categoryId, categoryName, level, eventsCount, children } = item
return (
<Leaf
key={categoryId}
categoryId={categoryId}
name={categoryName}
level={level}
eventsCount={eventsCount}
children={children}
isOpen={expanded.includes(categoryId)}
onClick={dispatch}
/>
)})}
</ul>
)
}
const Leaf = React.memo(({ name, categoryId, level, children, eventsCount, onClick, isOpen }) => {
const _onClick = () => {
onClick({ type: 'UPDATE_EXPANDED', categoryId, level });
}
return (
<li className={!isOpen && 'hidden'}>
<button onClick={_onClick}>
<span>{name}</span>
</button>
{children.length ? <Branch items={children}/> : ''}
</li>
)
})
export default Leaf
export default Branch;