React - same callback for each component in array - javascript

Lets say I have a components array in my React app:
const deleteProject = useCallback(project => {
// something
}, []);
return (
projects.map(p => (
<button onClick={() => deleteProject(p)}>Delete</button>
);
);
Is there any way I could use just deleteProject function without wrapping it into separate callbacks i.e. {} => {} for each component? This is for performance purposes. I mean something like:
<button onClick={deleteProject}>Delete</button>
And then in deleteProject somehow I'd need to determine which project to delete, but how? It only takes click event as argument

If you have a long projects list and see performance issues you could define DeleteButton component to avoid button re-rendering
const DeleteButton = ({project, deleteProject}) => {
const onClick = useCallback(
() => deleteProject(project),
[project, deleteProject],
);
return <button onClick={onClick}>Delete</button>
}
const YourComponent = ({projects, deleteProject}) => (
<>
{projects.map(project => <DeleteButton {...{project, deleteProject}}/>)
</>
)

You can achieve by assigning project identifier to button as below
const deleteProject = event => {
projectId = event.target.id;
// Delete project here using id
}
return (
projects.map(p => (
<button id={p.id} onClick={deleteProject}>Delete</button>
);

Related

Avoid Inline Function Definition in the Render Function

I saw many article said that define inline function in render function in react can cause to performance issue.
Therefore they recommend to define the function outside of the render function and use it where i need (onClick etc).
I built a sample code that i have a list of button and each button will increase the state by the index in the list, But its throw error.
How i can pass parameter and not use inline function in onClick
const App = () => {
const [number, setNumber] = useState(1);
const increaseNumber = (num) => {
setNumber((prevState) => prevState + num);
};
return (
<div>
{[...Array(5)].map((item, index) => (
<button key={index} onClick={increaseNumber(index)}>
{`increase by ${index}`}
</button>
))}
<div>{number}</div>
</div>
);
};
export default App;
I'll preface my answer by saying you really should profile your application and identify specific performance issues before trying to optimize anything. In this case, you could avoid creating a new callback with each map iteration by using data attributes.
function App() {
const [number, setNumber] = useState(1);
const increaseNumber = (event) => {
const index = parseInt(event.target.dataset.index);
setNumber((prevState) => prevState + index);
};
return (
<div>
{[...Array(5)].map((item, index) => (
<button key={index} onClick={increaseNumber} data-index={index}>
{`increase by ${index}`}
</button>
))}
<div>{number}</div>
</div>
);
}
export default App;
Typically the only time you would care about creating a new callback per render is when the callback is used as a prop in a child component. Using something like useCallback can help to avoid unnecessary child renders in those cases.
Aproach 1: useMemo
When the arguments are fixed like it's your case, you may use useMemo:
import { useMemo, useState } from "react";
const indexes = [...Array(5)].map((_item, idx) => idx);
const App = () => {
const [number, setNumber] = useState(1);
const increaseNumber = useMemo(() => {
return indexes.map(index => () => setNumber(prevNumber => prevNumber + index));
}, [indexes]);
return (
<div>
{indexes.map(index => (
<button key={index} onClick={increaseNumber[index]}>
increase by {index}
</button>
))}
<div>{number}</div>
</div>
);
};
Approach 2: wraper component + useCallback
Create your own button component and pass the index:
const IncreaseButton = ({ setNumber, index }) => {
const increaseByIndex = useCallback(() => {
return setNumber(prevValue => prevValue + index);
}, [setNumber, index]);
return <button onClick={increaseByIndex}>increase by {index}</button>;
};
You can pass an item as a function that had been memoized to the onClick prop of react button elements.
const App = () => {
const [number, setNumber] = useState(1);
const increaseNumber = (num) => () => {
setNumber((prevState) => prevState + num);
};
const btns = useMemo(() => {
// here I am using lodash memoize function you may use your own
let inc = _.memoize(increaseNumber)
return Array(500).fill(0).map((_, index) => inc(index))
}, [])
return (
<div>
{btns.map((item, index) => (
<button key={index} onClick={item}>
{`increase by ${index}`}
</button>
))}
<div>{number}</div>
</div>
);
};

React : state is empty when trying to get it from child component

I got two react components. The first component is managing a list of the second component after making a call to my api and I tried to put a delete button to remove one component from the list.
I got a weird behavior : when I click on the remove document button the document is set to an empty array. And I got a log of an empty array for the documents.
Parent component :
export const DocumentEnvelope: React.FC<DocumentEnvelopeProps> = (props) => {
const [documents, setDocuments] = useState([]);
useEffect(() => {
setDocuments([]);
axios.get("/myurl").then(response => {
response.data.forEach(document => {
setDocuments((doc) => [...doc, <Document name={document.name} removeDocument={removeDocument}/>]);});
});
}, []);
const removeDocument = (name) => {
console.log(documents);
setDocuments(documents.filter(item => item.name !== name));
};
return (
<>
{documents}
</>
);
};
Child component :
interface DocumentProps {
removeDocument,
name: string
}
export const Document: React.FC<DocumentProps> = (props) => {
return (
<div>
My document
<button onClick={() => props.removeDocument(props.name)}>
Remove document
</button>
</div>
);
};
<Document name=document.name removeDocument={removeDocument}/>
This part is missing curly braces around document.name, consider using a different variable name as document is used to refer to the html document in javascript. I can also recommend storing the data in the state but not the components themselves.
You're filtering on the documents array as if it consisted of document objects. But you have set the documents array to list of Document React Elements.
Let your documents array consist pure JS objects i.e. a document as you're getting it from response instead of React elements. Use map in JSX to loop over documents and return <Document.../> elements.
I mean like the following :-
export const DocumentEnvelope: React.FC<DocumentEnvelopeProps> = (props) => {
const [documents, setDocuments] = useState([]);
useEffect(() => {
axios.get("/myurl").then(response => {
setDocuments(response.data);
}, []);
const removeDocument = (name) => {
setDocuments(documents.filter(document => document.name !== name));
};
return (
<>
{documents.map((document)=>
<Document name={document.name} removeDocument={removeDocument}/>
)}
</>
);
};

How to memoize a high order function while using useCallback?

Using React Hooks, when we want to memoize the creation of a function, we have the useCallback hook. So that we have:
const MyComponent = ({ dependancies }) => {
const memoizedFn = useCallback(() => {
/* ... */
}, [dependancies]);
return <ChildComponent onClick={memoizedFn} />;
}
My question is, how do we memoize the values of a high order function in a useCallback hook such as:
const MyComponent => ({ dependancies, anArray }) => {
const memoizedFnCreator = useCallback((id) => () => {
/* ... */
}, [dependancies]);
/*
* How do we make sure calling "memoizedFnCreator" does not result in
* the ChildComponent rerendering due to a new function being created?
*/
return (
<div>
{anArray.map(({ id }) => (
<ChildComponent key={id} onClick={memoizedFnCreator(id)} />
))}
</div>
);
}
Instead of passing a "creator-function" in the HoC you can pass down a function which takes in a id as argument and let the ChildComponent create it's own click handler
In the code example below notice that the onClick in the MyComponent no longer create a unique function, but reuses the same function across all the mapped elements, however the ChildComponent creates a unique function.
const ChildComponent = ({ itemId, onClick }) => {
// Create a onClick handler when calls `onClick` with the item's id
const handleClick = useCallback(() => {
onClick(itemId)
}, [onClick, itemId])
return <button onClick={handleClick}>Click me</button>
}
const MyComponent = ({ dependancies, anArray }) => {
// Memoize a function which takes in the id and performs some action
const handleItemClick = useCallback((id) => {
/* ... */
}, [dependancies]);
return (
<div>
{anArray.map(({ id }) => (
<ChildComponent key={id} itemId={id} onClick={handleItemClick} />
))}
</div>
);
}
Depending on what you are trying to achieve, the optimal solution might be entirely different, but here's one way to go about it:
const MyComponent = ({ dependencies, array }) => {
// create a memoized callbacks cache, where we will store callbacks.
// This cache will reset every time dependencies change.
const callbacks = useMemo(() => ({}), [dependencies]);
const createCallback = useCallback(
id => {
if (!callbacks[id]) {
// Cache the callback here...
callbacks[id] = () => {
/* ... */
};
}
// ...and return it.
return callbacks[id];
},
[dependencies, callbacks]
);
return (
<div>
{array.map(({ id }) => (
<ChildComponent key={id} onClick={createCallback(id)} />
))}
</div>
);
};
Since the cache resets as dependencies change, your callbacks will update accordingly.
If it were me, I could avoid higher order function as much as possible. In this case, I would have something like
const MyComponent => ({ dependancies, anArray }) => {
const memoizedFnCreator = useCallback((id, event) => {}, [dependancies]);
return (
<div>
{anArray.map(({ id }) => (
<ChildComponent key={id} onClick={(event) => memoizedFnCreator(id,event)}/>
))}
</div>
);
}

React conditionally render component using an outside function

I'm working on a modal function in an application. Since the app has different modals, I have a function which handles the open & close state of various windows:
OpenItem.jsx
const OpenItem = ({ toggle, content }) => {
const [isShown, setIsShown] = useState(false);
const hide = () => setIsShown(false);
const show = () => setIsShown(true);
return (
<>
{toggle(show)}
{isShown && content(hide)}
</>
);
};
export default OpenItem;
Header.jsx
Now in my main component, I want to to use this function with another component:
const Header = () => {
return (
<div>
<OpenItem
toggle={(show) => <Button onClick={show}>icon</Button>}
content={(hide) => (
// Component to hide:
<ComponentToShowOrHide onClick={hide} />
)}
/>
</div>
);
};
export default Header;
This works fine, except that instead of having the {hide} function as a part of the imported component, I want to toggle the view in <Button onClick={show}>icon</Button>
My idea is to conditionally render the show or hide in the button instead of rendering it in the component, but I'm not quite sure how to do that since I haven't used an outside function to control a function in a component.
Simply write a function that toggles the state rather than sets it to a value.
const OpenItem = ({ toggle, content }) => {
const [isShown, setIsShown] = useState(false);
return (
<>
{toggle(() => setIsShown(prevState => !prevState))}
</>
);
};
export default OpenItem;

Force a single re-render with useSelector

This is a follow-up to Refactoring class component to functional component with hooks, getting Uncaught TypeError: func.apply is not a function
I've declared a functional component Parameter that pulls in values from actions/reducers using the useSelector hook:
const Parameter = () => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter)
const dispatch = useDispatch();
const drawerOpen = useSelector(state => state.filterIconClick);
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
return (
prevState => ({
...prevState,
parameterCurrent: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
//some code describing an alert
});
})
.otherwise(function (err) {
alert(
//some code describing a different alert
);
});
}
);
};
const classes = useStyles();
return (
<div>
{drawerOpen ? (
Object.keys(parameterSelect).map((key, index) => {
return (
<div>
<FormControl component="fieldset">
<FormLabel className={classes.label} component="legend">
{key}
</FormLabel>
{parameterSelect[key].map((valKey, valIndex) => {
return (
<RadioGroup
aria-label="parameter"
name="parameter"
value={parameterCurrent[key]}//This is where the change should be reflected in the radio button
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
<FormControlLabel
className={classes.formControlparams}
value={valKey}
control={
<Radio
icon={
<RadioButtonUncheckedIcon fontSize="small" />
}
className={clsx(
classes.icon,
classes.checkedIcon
)}
/>
}
label={valKey}
/>
</RadioGroup>
);
})}
</FormControl>
<Divider className={classes.divider} />
</div>
);
})
) : (
<div />
)
}
</div >
)
};
export default Parameter;
What I need to have happen is for value={parameterCurrent[key]} to rerender on handleParameterChange (the handleChange does update the underlying dashboard data, but the radio button doesn't show as being selected until I close the main component and reopen it). I thought I had a solution where I forced a rerender, but because this is a smaller component that is part of a larger one, it was breaking the other parts of the component (i.e. it was re-rendering and preventing the other component from getting state/props from it's reducers). I've been on the internet searching for solutions for 2 days and haven't found anything that works yet. Any help is really apprecaited! TIA!
useSelector() uses strict === reference equality checks by default, not shallow equality.
To use shallow equal check, use this
import { shallowEqual, useSelector } from 'react-redux'
const selectedData = useSelector(selectorReturningObject, shallowEqual)
Read more
Ok, after a lot of iteration, I found a way to make it work (I'm sure this isn't the prettiest or most efficient, but it works, so I'm going with it). I've posted the code with changes below.
I added the updateState and forceUpdate lines when declaring the overall Parameter function:
const Parameter = () => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter);
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const dispatch = useDispatch();
const drawerOpen = useSelector(state => state.filterIconClick);
Then added the forceUpdate() line here:
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
return (
prevState => ({
...prevState,
parameterCurrent: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
//some code describing an alert
});
})
.otherwise(function (err) {
alert(
//some code describing a different alert
);
});
forceUpdate() //added here
}
);
};
Then called forceUpdate in the return statement on the item I wanted to re-render:
<RadioGroup
aria-label="parameter"
name="parameter"
value={forceUpdate, parameterCurrent[key]}//added forceUpdate here
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
I've tested this, and it doesn't break any of the other code. Thanks!

Categories