How to memoize a high order function while using useCallback? - javascript

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>
);
}

Related

React-Hooks- How to assign an unique "ref" to every rendered item in a renderItem using FlatList?

I am trying to convert a class component to a function component and struggling with assigning the refs to each rendered item in a Flatlist.
This is the original class component.
...
constructor(props) {
super(props);
this.cellRefs = {};
}
....
_renderItem = ({ item }) => {
return (
<Item
ref={ref => {
this.cellRefs[item.id] = ref;
}}
{...item}
/>
);
};
...
Assuming both your Item and the component rendering the FlatList need to be functional components, you need to take care of 2 things
Add dynamic refs to each Item component
Make sure that the Item component uses useImperativeHandle with forwardRef to expose functions
const App = () => {
const cellRefs = useRef({}) // Adding an object as we need more than one ref
const _renderItem = ({ item }) => {
return (
<Item
ref={ref => {
cellRefs.current[item.id] = ref;
}}
{...item}
/>
);
};
....
}
Post that you need to change your Item component like
const Item = React.forwardRef((props, ref) => {
...
const handleClick = () => {};
useImperativeHandle(ref, () => ({
// values that need to accessible by component using ref, Ex
handleClick,
}))
...
})
P.S. If Item is not a functional component, you can avoid the second step
do something like that (from react doc)
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

Conditionally updating child component's state in event-handler passed by Parent-class

const Child = (props) => {
const [val, setVal] = useState(props.val);
const handleCreate = (newData) => new Promise((resolve, reject) => {
setTimeout(() => {
{
const transactions = JSON.parse(JSON.stringify(tableData));
const clean_transaction = getCleanTransaction(newData);
const db_transaction = convertToDbInterface(clean_transaction);
transactions.push(clean_transaction);
// The below code makes post-request to 2 APIs synchronously and conditionally updates the child-state if calls are successful.
**categoryPostRequest(clean_transaction)
.then(category_res => {
console.log('cat-add-res:', category_res);
transactionPostRequest(clean_transaction)
.then(transaction_res => {
addToast('Added successfully', { appearance: 'success'});
**setVal(transactions)**
}).catch(tr_err => {
addToast(tr_err.message, {appearance: 'error'});
})
}).catch(category_err => {
console.log(category_err);
addToast(category_err.message, {appearance: 'error'})
});**
}
resolve()
}, 1000)
});
return (
<MaterialTable
title={props.title}
data={val}
editable={{
onRowAdd: handleCreate
}}
/>
);
}
const Parent = (props) => {
// some other stuff to generate val
return (
<Child val={val}/>
);
}
I am struggling to achieve this:
I'd like to move the post-request part of the function in handleCreate (bold-section), over to the Parent-component that can be called by the Child-class.
The idea is to make the Component abstract and re-usable by other similar Parent-classes.
Create the function in the parent, and pass it to the child in the props:
const Parent = (props) => {
// The handler
const onCreate = /*...*/;
// some other stuff
return (
<Child ...props onCreate={onCreate}/>
);
}
Then have the child call the function with any parameters it needs (there don't seem to be any in your example, you're not using val in it for instance):
return (
<MaterialTable
title={props.title}
data={val}
editable={{
onRowAdd: props.onCreate // Or `onRowAdd: () => props.onCreate(parameters, go, here)`
}}
/>
);
Side note: There's no reason to copy props.val to val state member within the child component, just use props.val.
Side note 2: Destructuring is often handy with props:
const Child = ({val, onCreate}) => {
// ...
};
Side note 3: You have your Parent component calling Child with all of its props, via ...props:
return (
<Child ...props onCreate={onCreate}/>
);
That's generally not best. Only pass Child what it actually needs, in this case, val and onCreate:
return (
<Child val={props.val} onCreate={onCreate}/>
);

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!

Can i set state in parent from child using useEffect hook in react

I have a set of buttons in a child component where when clicked set a corresponding state value true or false. I have a useEffect hook in this child component also with dependencies on all these state values so if a button is clicked, this hook then calls setFilter which is passed down as a prop from the parent...
const Filter = ({ setFilter }) => {
const [cycling, setCycling] = useState(true);
const [diy, setDiy] = useState(true);
useEffect(() => {
setFilter({
cycling: cycling,
diy: diy
});
}, [cycling, diy]);
return (
<Fragment>
<Row>
<Col>
<Button block onClick={() => setCycling(!cycling)}>cycling</Button>
</Col>
<Col>
<Button block onClick={() => setdIY(!DIY)}>DIY</Button>
</Col>
</Row>
</Fragment>
);
};
In the parent component I display a list of items. I have two effects in the parent, one which does an initial load of items and then one which fires whenever the filter is changed. I have removed most of the code for brevity but I think the ussue I am having boils down to the fact that on render of my ItemDashboard the filter is being called twice. How can I stop this happening or is there another way I should be looking at this.
const ItemDashboard = () => {
const [filter, setFilter] = useState(null);
useEffect(() => {
console.log('on mount');
}, []);
useEffect(() => {
console.log('filter');
}, [filter]);
return (
<Container>..
<Filter setFilter={setFilter} />
</Container>
);
}
I'm guessing, you're looking for the way to lift state up to common parent.
In order to do that, you may bind event handlers of child components (passed as props) to desired callbacks within their common parent.
The following live-demo demonstrates the concept:
const { render } = ReactDOM,
{ useState } = React
const hobbies = ['cycling', 'DIY', 'hiking']
const ChildList = ({list}) => (
<ul>
{list.map((li,key) => <li {...{key}}>{li}</li>)}
</ul>
)
const ChildFilter = ({onFilter, visibleLabels}) => (
<div>
{
hobbies.map((hobby,key) => (
<label {...{key}}>{hobby}
<input
type="checkbox"
value={hobby}
checked={visibleLabels.includes(hobby)}
onChange={({target:{value,checked}}) => onFilter(value, checked)}
/>
</label>))
}
</div>
)
const Parent = () => {
const [visibleHobbies, setVisibleHobbies] = useState(hobbies),
onChangeVisibility = (hobby,visible) => {
!visible ?
setVisibleHobbies(visibleHobbies.filter(h => h != hobby)) :
setVisibleHobbies([...visibleHobbies, hobby])
}
return (
<div>
<ChildList list={visibleHobbies} />
<ChildFilter onFilter={onChangeVisibility} visibleLabels={visibleHobbies} />
</div>
)
}
render (
<Parent />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>
Yes, you can, useEffect in child component which depends on the state is also how you typically implement a component which is controlled & uncontrolled:
const NOOP = () => {};
// Filter
const Child = ({ onChange = NOOP }) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
onChange(counter);
}, [counter, onChange]);
const onClick = () => setCounter(c => c + 1);
return (
<div>
<div>{counter}</div>
<button onClick={onClick}>Increase</button>
</div>
);
};
// ItemDashboard
const Parent = () => {
const [value, setState] = useState(null);
useEffect(() => {
console.log(value);
}, [value]);
return <Child onChange={setState} />;
};

React - same callback for each component in array

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>
);

Categories