Is there a good way to use React hooks inside a callback or render prop? - reactjs

Often times I find myself creating or using components where callbacks are used to create part of the component tree, as in these two simplified examples:
// Example 1: Render an array of components
const ExampleComponent1 = () => (
<ul>
{items.map((item) => <li key={item.id} style={{ paddingTop: item.paddingTop }}>{item.label}</li>)}
</ul>
);
// Example 2: Use render props
const ExampleComponent2 = () => (
<Dialog
renderHeader={useCallback((dialogCtx) => 'Header', [])}
renderBody={useCallback((dialogCtx) => <button onClick={() => dialogCtx.closeDialog(null)}>Test</button>, [])}
/>
);
As you can see, the callback results in both cases are dependent on the callback argument. In order to avoid a new style object (in example 1) and a new onClick function (in example 2) to be created on every render, I should wrap them in useMemo/useCallback, but that is not possible here because they are created inside the callbacks rather than on the root level of the component.
The only way that I'm aware of to work around this problem is to create designated components for each callback:
// Example 1: Render an array of components
const ExampleComponent1 = () => (
<ul>
{items.map((item) => <ExampleComponent1Item key={item.id} item={item}/>)}
</ul>
);
const ExampleComponent1Item = ({ item }) => <li style={useMemo(() => ({ paddingTop: item.paddingTop }), [item.paddingTop])}>{item.label}</li>;
// Example 2: Use render props
const ExampleComponent2 = () => (
<Dialog
renderHeader={useCallback((dialogCtx) => <ExampleComponent2Header dialogCtx={dialogCtx}/>, [])}
renderBody={useCallback((dialogCtx) => <ExampleComponent2Body dialogCtx={dialogCtx}/>, [])}
/>
);
const ExampleComponent2Header = ({ dialogCtx }) => 'Header';
const ExampleComponent2Body = ({ dialogCtx }) => <button onClick={useCallback(() => dialogCtx.closeDialog(null), [dialogCtx.closeDialog])}>Test</button>;
You can already see in this simplified example how splitting up the app in such a way creates a lot of additional code and makes the app much harder to read. In more complex scenarios where a lot of props from the parent component need to be reused in the sub component, the sub components will become even more bulky, particularly when prop types need to be defined as well.
It seems to me that both of these example are rather common use cases, since callbacks are the only way to generate a list of components from an array in React, and render props are a common pattern in more complex components. I'm wondering:
Is there any way how I could write the above example without splitting off the callback results into separate components? For example some way to use hooks directly inside the callbacks.
Is there an alternative pattern for mapping an array to a list of components that doesn't rely on callbacks, so hooks can be used?
Is there an alternative pattern to render props that makes it easier to use hooks?
Update: To make it clear, I’m looking for a programming pattern, not specific problems in the simplified example code above.

I don't understand what's wrong with example 1.
const ExampleComponent1 = () => (
<ul>
{items.map((item) => <li key={item.id} style={{ paddingTop: item.paddingTop }}>{item.label}</li>)}
</ul>
);
This is perfectly fine in React. Until you can see that the style prop is causing performance issues, you shouldn't try to optimize it.
Version with hooks, in case you somehow really need it:
const ExampleComponent1Item = ({ item }) => {
const style = useMemo(() => ({ paddingTop: item.paddingTop }), [item.paddingTop]);
return (<li style={style}>{item.label}</li>);
};
const ExampleComponent1 = () => (
<ul>
{items.map((item) => <ExampleComponent1Item key={item.id} item={item}/>)}
</ul>
);
The same goes for example 2:
const ExampleComponent2 = () => {
const renderHeader = useCallback((dialogCtx) => 'Header', []);
const renderBody = useCallback((dialogCtx) => <button onClick={() => dialogCtx.closeDialog(null)}>Test</button>, []);
return (<Dialog renderHeader={renderHeader} renderBody=renderBody}/>);
};
If you want to avoid the onClick function to be recreated each time renderBody is called, you can do:
const ExampleComponent2Body = {( dialogCtx }) => {
const onClick = useCallback(() => dialogCtx.closeDialog(null), [dialogCtx.closeDialog]);
return (<button onClick={onClick}>Test</button>);
};
const ExampleComponent2 = () => {
const renderHeader = useCallback((dialogCtx) => 'Header', []);
const renderBody = useCallback((dialogCtx) => <ExampleComponent2Body dialogCtx={dialogCtx}/>, []);
return (<Dialog renderHeader={renderHeader} renderBody=renderBody}/>);
};

Related

React (Native) how to make a component reusable when passing different data to iterate and callbacks from parent as well as child to grandchild?

I have a component thats opening and showing a modal that I want to reuse because almost everything I need in multiple places. Whats different is 1. data I am iterating through (property names are different) and 2. the button that triggers the modal has different styling. The problem is also that from the parent components I pass a callback, however, I also need to pass a callback to the part where I iterate/render data another callback coming from child component which is why I cannot just render the data iteration as children prop (thus always passing different data). I tried to implement a renderprop but also failed. I hope I explained not too confusing!! How do I do it?
const Parent1 = () => {
const [reportedLine, setReportedLine] = useState(null);
const [availableLines, setAvailableLines] = useState([]);
const [searchResultId, setSearchResultId] = useState('');
return (
<AvailableLinesSelector
data={availableLines}
disabled={searchResultId}
onSelect={setReportedLine}
/>
)
};
const Parent2 = () => {
const [line, setLine] = useState(null);
return (
<AvailableLinesSelector
data={otherData}
disabled={item}
onSelect={setLine}
/>
)
};
const AvailableLinesSelector = ({data, onSelect, disabled}) => {
const [isVisible, setIsVisible] = useState(false);
const [selectedLine, setSelectedLine] = useState('Pick the line');//placeholder should also be flexible
const handleCancel = () => setIsVisible(false);
const handleSelect = (input) => {
onSelect(input)
setSelectedLine(input)
setIsVisible(false);
};
return (
<View>
<Button
title={selectedLine}
//a lot of styling that will be different depending on which parent renders
disabled={disabled}
onPress={() => setIsVisible(true)}
/>
<BottomSheet isVisible={isVisible}>
<View>
{data && data.map(line => (
<AvailableLine //here the properties as name, _id etc will be different depending on which parent renders this component
key={line._id}
line={line.name}
onSelect={handleSelect}
/>
))}
</View>
<Button onPress={handleCancel}>Cancel</Button>
</BottomSheet>
</View>
)
};
You can clone the children and pass additional props like:
React.Children.map(props.children, (child) => {
if (!React.isValidElement(child)) return child;
return React.cloneElement(child, {...child.props, myCallback: callback});
});

how to test that props are passed to child component with react testing library and jest? [duplicate]

My component looks something like this: (It has more functionality as well as columns, but I have not included that to make the example simpler)
const WeatherReport: FunctionComponent<Props> = ({ cityWeatherCollection, loading, rerender }) => {
/* some use effects skipped */
/* some event handlers skipped */
const columns = React.useMemo(() => [
{
header: 'City',
cell: ({ name, title }: EnhancedCityWeather) => <Link to={`/${name}`} className="city">{title}</Link>
},
{
header: 'Temp',
cell: ({ temperature }: EnhancedCityWeather) => (
<div className="temperature">
<span className="celcius">{`${temperature}°C`}</span>
<span className="fahrenheit">{` (~${Math.round(temperature * (9 / 5)) + 32}°F)`}</span>
</div>
)
},
{
header: '',
cell: ({ isFavorite } : EnhancedCityWeather) => isFavorite && (
<HeartIcon
fill="#6d3fdf"
height={20}
width={20}
/>
),
},
], []);
return (
<Table columns={columns} items={sortedItems} loading={loading} />
);
};
Now, I wrote some tests like this:
jest.mock('../../../components/Table', () => ({
__esModule: true,
default: jest.fn(() => <div data-testid="Table" />),
}));
let cityWeatherCollection: EnhancedCityWeather[];
let loading: boolean;
let rerender: () => {};
beforeEach(() => {
cityWeatherCollection = [/*...some objects...*/];
loading = true;
rerender = jest.fn();
render(
<BrowserRouter>
<WeatherReport
cityWeatherCollection={cityWeatherCollection}
loading={loading}
rerender={rerender}
/>
</BrowserRouter>
);
});
it('renders a Table', () => {
expect(screen.queryByTestId('Table')).toBeInTheDocument();
});
it('passes loading prop to Table', () => {
expect(Table).toHaveBeenCalledWith(
expect.objectContaining({ loading }),
expect.anything(),
);
});
it('passes items prop to Table after sorting by isFavorite and then alphabetically', () => {
expect(Table).toHaveBeenCalledWith(
expect.objectContaining({
items: cityWeatherCollection.sort((item1, item2) => (
+item2.isFavorite - +item1.isFavorite
|| item1.name.localeCompare(item2.name)
)),
}),
expect.anything(),
);
});
If you check my component, it has a variable called columns. I am assigning that variable to Table component.
I think, I should test that columns are being passed as props to the Table component. Am I thinking right? If so, can you please tell me how can I write a test case for that?
Also, it will be helpful if you can suggest me how can i test each cell declared inside columns property.
It is not recommended to test implementation details, such as component props, with React Testing Library. Instead you should be asserting on the screen content.
Recommended
expect(await screen.findByText('some city')).toBeInTheDocument();
expect(screen.queryByText('filtered out city')).not.toBeInTheDocument();
Not Recommended
If you want to test props anyways, you can try the sample code below. Source
import Table from './Table'
jest.mock('./Table', () => jest.fn(() => null))
// ... in your test
expect(Table).toHaveBeenCalledWith(props, context)
You might consider this approach mainly on the two following scenarios.
You already tried the recommended approach but you noticed the component is:
using legacy code and because of that it makes testing very hard. Refactoring the component would also take too long or be too risky.
is very slow and it drastically increases the testing time. The component is also already tested somewhere else.
have a look at a very similar question here
You can use the props() method, doing something like this:
expect(Table.props().propYouWantToCheck).toBeFalsy();
Just doing your component.props() then the prop you want, you can make any assert with it.

What is the right place to call a function before render in React?

I have some issue on understandsing the lifecycle in React, so iam using useEffects() since i do understand that it was the right way to call a method before the component rendered (the replacement for componentDidMount ).
useEffect(() => {
tagSplit = tagArr.split(',');
});
And then i call tagSplit.map() function on the component, but it says that tagSplit.map is not a function
{tagSplit.map((item, index) => (
<div className="styles" key={index}>
{item}
</div>
))}
Is there something wrong that i need to fix or was it normal ?
useEffect runs AFTER a render and then subsequently as the dependencies change.
So yes, if you have tagSplit as something that doesn't support a map function initially, it'll give you an error from the first render.
If you want to control the number of times it runs, you should provide a dependency array.
From the docs,
Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update. (We will later talk about how to customize this.) Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.
This article from Dan Abramov's blog should also help understand useEffect better
const React, { useState, useEffect } from 'react'
export default () => {
const [someState, setSomeState] = useState('')
// this will get reassigned on every render
let tagSplit = ''
useEffect(() => {
// no dependencies array,
// Runs AFTER EVERY render
tagSplit = tagArr.split(',');
})
useEffect(() => {
// empty dependencies array
// RUNS ONLY ONCE AFTER first render
}, [])
useEffect(() => {
// with non-empty dependency array
// RUNS on first render
// AND AFTER every render when `someState` changes
}, [someState])
return (
// Suggestion: add conditions or optional chaining
{tagSplit && tagSplit.map
? tagSplit.map((item, index) => (
<div className='styles' key={index}>
{item}
</div>
))
: null}
)
}
you can do something like this .
function App() {
const [arr, setArr] = useState([]);
useEffect(() => {
let tagSplit = tagArr.split(',');
setArr(tagSplit);
}, []);
return (
<>
{arr.map((item, index) => (
<div className="styles" key={index}>
{item}
</div>
))}
</>
)
}
Answering the question's title:
useEffect runs after the first render.
useMemo runs before the first render.
If you want to run some code once, you can put it inside useMemo:
const {useMemo, Fragment} = React
const getItemsFromString = items => items.split(',');
const Tags = ({items}) => {
console.log('rendered')
const itemsArr = useMemo(() => getItemsFromString(items), [items])
return itemsArr.map((item, index) => <mark style={{margin: '3px'}} key={index}>{item}</mark>)
}
// Render
ReactDOM.render(<Tags items='foo, bar, baz'/>, root)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
For your specific component, it's obvious there is no dilema at all, as you can directly split the string within the returned JSX:
return tagArr.split(',').map((item, index) =>
<div className="styles" key={index}>
{item}
</div>
)
But for more complex, performance-heavy transformations, it is best to run them only when needed, and use a cached result by utilizing useMemo

useLoopCallback -- useCallback hook for components created inside a loop

I'd like to start a discussion on the recommended approach for creating callbacks that take in a parameter from a component created inside a loop.
For example, if I'm populating a list of items that will have a "Delete" button, I want the "onDeleteItem" callback to know the index of the item to delete. So something like this:
const onDeleteItem = useCallback(index => () => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<div>
<span>{item}</span>
<button type="button" onClick={onDeleteItem(index)}>Delete</button>
</div>
)}
</div>
);
But the problem with this is that onDeleteItem will always return a new function to the onClick handler, causing the button to be re-rendered, even when the list hasn't changed. So it defeats the purpose of useCallback.
I came up with my own hook, which I called useLoopCallback, that solves the problem by memoizing the main callback along with a Map of loop params to their own callback:
import React, {useCallback, useMemo} from "react";
export function useLoopCallback(code, dependencies) {
const callback = useCallback(code, dependencies);
const loopCallbacks = useMemo(() => ({map: new Map(), callback}), [callback]);
return useCallback(loopParam => {
let loopCallback = loopCallbacks.map.get(loopParam);
if (!loopCallback) {
loopCallback = (...otherParams) => loopCallbacks.callback(loopParam, ...otherParams);
loopCallbacks.map.set(loopParam, loopCallback);
}
return loopCallback;
}, [callback]);
}
So now the above handler looks like this:
const onDeleteItem = useLoopCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
This works fine but now I'm wondering if this extra logic is really making things faster or just adding unnecessary overhead. Can anyone please provide some insight?
EDIT:
An alternative to the above is to wrap the list items inside their own component. So something like this:
function ListItem({key, item, onDeleteItem}) {
const onDelete = useCallback(() => {
onDeleteItem(key);
}, [onDeleteItem, key]);
return (
<div>
<span>{item}</span>
<button type="button" onClick={onDelete}>Delete</button>
</div>
);
}
export default function List(...) {
...
const onDeleteItem = useCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<ListItem key={index} item={item} onDeleteItem={onDeleteItem} />
)}
</div>
);
}
Performance optimizations always come with a cost. Sometimes this cost is lower than the operation to be optimized, sometimes is higher. useCallback it's a hook very similar to useMemo, actually you can think of it as a specialization of useMemo that can only be used in functions. For example, the bellow statements are equivalents
const callback = value => value * 2
const memoizedCb = useCallback(callback, [])
const memoizedWithUseMemo = useMemo(() => callback, [])
So for now on every assertion about useCallback can be applied to useMemo.
The gist of memoization is to keep copies of old values to return in the event we get the same dependencies, this can be great when you have something that is expensive to compute. Take a look at the following code
const Component = ({ items }) =>{
const array = items.map(x => x*2)
}
Uppon every render the const array will be created as a result of a map performed in items. So you can feel tempted to do the following
const Component = ({ items }) =>{
const array = useMemo(() => items.map(x => x*2), [items])
}
Now items.map(x => x*2) will only be executed when items change, but is it worth? The short answer is no. The performance gained by doing this is trivial and sometimes will be more expensive to use memoization than just execute the function each render. Both hooks(useCallback and useMemo) are useful in two distinct use cases:
Referencial equality
When you need to ensure that a reference type will not trigger a re render just for failing a shallow comparison
Computationally expensive operations(only useMemo)
Something like this
const serializedValue = {item: props.item.map(x => ({...x, override: x ? y : z}))}
Now you have a reason to memoized the operation and lazily retrieve the serializedValue everytime props.item changes:
const serializedValue = useMemo(() => ({item: props.item.map(x => ({...x, override: x ? y : z}))}), [props.item])
Any other use case is almost always worth to just re compute all values again, React it's pretty efficient and aditional renders almost never cause performance issues. Keep in mind that sometimes your efforts to optimize your code can go the other way and generate a lot of extra/unecessary code, that won't generate so much benefits (sometimes will only cause more problems).
The List component manages it's own state (list) the delete functions depends on this list being available in it's closure. So when the list changes the delete function must change.
With redux this would not be a problem because deleting items would be accomplished by dispatching an action and will be changed by a reducer that is always the same function.
React happens to have a useReducer hook that you can use:
import React, { useMemo, useReducer, memo } from 'react';
const Item = props => {
//calling remove will dispatch {type:'REMOVE', payload:{id}}
//no arguments are needed
const { remove } = props;
console.log('component render', props);
return (
<div>
<div>{JSON.stringify(props)}</div>
<div>
<button onClick={remove}>REMOVE</button>
</div>
</div>
);
};
//wrap in React.memo so when props don't change
// the ItemContainer will not re render (pure component)
const ItemContainer = memo(props => {
console.log('in the item container');
//dispatch passed by parent use it to dispatch an action
const { dispatch, id } = props;
const remove = () =>
dispatch({
type: 'REMOVE',
payload: { id },
});
return <Item {...props} remove={remove} />;
});
const initialState = [{ id: 1 }, { id: 2 }, { id: 3 }];
//Reducer is static it doesn't need list to be in it's
// scope through closure
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
//remove the id from the list
return state.filter(
item => item.id !== action.payload.id
);
}
return state;
};
export default () => {
//initialize state and reducer
const [list, dispatch] = useReducer(
reducer,
initialState
);
console.log('parent render', list);
return (
<div>
{list.map(({ id }) => (
<ItemContainer
key={id}
id={id}
dispatch={dispatch}
/>
))}
</div>
);
};

Calling react context prop (function) from components function

I have a Context Consumer which works like this:
<ToastConsumer>
{({ openToast }) => (
<button onClick={() => openToast('You clicked Button A!')}>
Button A
</button>
)}
</ToastConsumer>
But I want to add some extra logic on the click handler and move the openToast Consumer function like this:
upVote = () => {
if (!this.state.hasVoted) {
this.setState({
hasVoted: true,
rating: this.state.rating + 1,
});
this.vote(this.state.rating + 1);
}
this.openToast // not working???
};
<ToastConsumer>
{({ openToast }) => (
<div className="vote-button">
<span
className="vote-up vote-action cursor-pointer"
onClick={this.upVote}
>
👍 +1...
All of the examples provided for the Context API seem to be a simple click handler and I cant workout how to acheive a more complex example like this one.
One way to achieve this is to pass your openToast function into your new handler. You can do this either by wrapping the onClick in a function, or by currying your upVote function.
Examples:
Wrapping in a function:
upVote = (openToast) => {
onClick={() => this.upVote(openToast)}
Currying upVote:
upVote = (openToast) => () => {
onClick={this.upVote(openToast)}
openToast needs to be provided to upVote as an argument (as another answer already mentions), upVote becomes higher-order function:
upVote = openToast => () => {
// ...
openToast();
}
And used like:
<span onClick={this.upVote(openToast)}>
A way to avoid this complexity with context consumers is to make a context available in class instance. This can be done with contextType:
static contextType = ToastContext;
upVote = openToast => () => {
// ...
this.context.openToast();
}
A downside is that this restricts a component to be used with one context.
Or a context can be provided to a component with a HOC:
const withToast = Comp => props => (
<ToastConsumer>
{({ openToast }) => <Comp openToast={openToast} ...props/>}
</ToastConsumer>
);
Then a component that was connected to a context with withToast(MyComponent) receives openToast as a prop:
upVote = openToast => () => {
// ...
this.props.openToast();
}

Resources