I have two similar React components and the way that I handled them is by using the first component in the other one. It works, but not really sure if this is a bad practice or not.
const Component1({titles, handleClick, isSpecial}) => {
const toggle = (index) => {
handleClick(index)
}
return (
{titles.map((title, index) => (
<button onClick={() => toggle(index)}>Click me</button>
))}
)
}
import Component1
const Component2({titles, updateTitle}) => {
const alteredTitles = titles.map((title) => {
const alteredTitle = title.toUppercase();
return alteredTitle;
}
const handleClick = (index) => {
const alteredTitle = alteredTitles[index];
updateTitle(alteredTitle);
}
return (
<Component1 titles={alteredTitles} handleClick={handleClick} isSpecial={true}
)
}
Is this a poor way to do this? My other options of my current knowledge would be to duplicate most of the functionality of Component1 into Component2 or to add conditionals into Component1. The way I have done it makes my code feel more clean I guess, but I feel like it is the wrong way to go about it, but don't really know why.
Related
I'm not sure the title is correct, so let me try to explain what I'm trying to achieve.
Let's say I have a flow in my application that has 3 steps in it, so I create a component (let's call it Stepper) with 3 child components where each child is a component that renders the corresponding step.
I want to expose a custom hook to the child components of Stepper, let's call it useStepper.
This is how Stepper would look like (JSX-wise):
export const Stepper = (props) => {
...some logic
return (
<SomeWrapper>
{props.children}
</SomeWrapper>
);
};
so I can make components like this:
export SomeFlow = () => {
return (
<Stepper>
<StepOne />
<StepTwo />
<StepThree />
</Stepper>
);
};
Now this is how I want things to work inside Stepper's children, let's take StepThree as an example:
export const StepThree = () => {
const exposedStepperData = useStepper();
... some logic
return (
...
);
};
Now, it's important that the Stepper will be reusable; That means - each Stepper instance should have its own data/state/context that is exposed through the useStepper hook.
Different Stepper instances should have different exposed data.
Is it possible to achieve this? I tried to use Context API but I was not successful. It's also weird that I couldn't find anything about it on the internet, maybe I searched wrong queries as I don't know what patten it is (if it exists).
Note:
I achieved a similar behavior through injected props from parent to its children, but it's not as clean as I want it to be, especially with Typescript.
I recently came across something like this, it was solved by pouring all the components/steps in an array and let the hook manage which component/step to show. If you want it to be more reusable you could pass in the children to the array.
I hope this helps you in the right direction
useStepper.ts
import { ReactElement, useState } from "react";
export const useStepper = (steps: ReactElement[]) => {
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const next = () => {
setCurrentStepIndex((i: number) => {
if (i >= steps.length - 1) return i;
return i + 1;
});
};
const back = () => {
setCurrentStepIndex((i: number) => {
if (i <= 0) return i;
return i - 1;
});
};
const goTo = (index: number) => {
setCurrentStepIndex(index);
};
return {
currentStepIndex,
step: steps[currentStepIndex],
steps,
isFirstStep: currentStepIndex === 0,
isLastStep: currentStepIndex === steps.length - 1,
goTo,
next,
back,
};
};
Stepper.tsx
// const { currentStepIndex, step, isFirstStep, isLastStep, back, next } =
// useStepper([<StepOne />, <StepTwo />, <StepThree />]);
const { currentStepIndex, step, isFirstStep, isLastStep, back, next } =
useStepper([...children]);
return (
<div>
{!isFirstStep && <button onClick={back}>Back</button>}
{step}
<button onClick={next}>{isLastStep ? "Finish" : "Next"}</button>
</div>
);
My goal is very simple. I am just looking to set my react context from within a reusable function-only (stateless?) react component.
When this reusable function gets called it will set the context (state inside) to values i provide. The problem is of course you can't import react inside a function-only component and hence I cannot set the context throughout my app.
There's nothing really to show its a simple problem.
But just in case:
<button onCLick={() => PlaySong()}></button>
export function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //cannot call useContext in this component
}
If i use a regular react component, i cannot call this function onClick:
export default function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //fine
}
But:
<button onCLick={() => <PlaySong />}></button> //not an executable function
One solution: I know i can easily solve this problem by simply creating a Playbtn component and place that in every song so it plays the song. The problem with this approach is that i am using a react-player library so i cannot place a Playbtn component in there...
You're so close! You just need to define the callback inside the function component.
export const PlaySongButton = ({...props}) => {
const {setCurrentSong} = useContext(StoreContext);
const playSong = () => {
setCurrentSong("some song");
}
return (
<button
{...props}
onClick={() => playSong()}
/>
)
}
If you want greater re-usability, you can create custom hooks to consume your context. Of course where you use these still has to follow the rules of hooks.
export const useSetCurrentSong = (song) => {
const {setCurrentSong} = useContext(StoreContext);
setCurrentSong(song);
}
It is possible to trigger a hook function by rendering a component, but you cannot call a component like you are trying to do.
const PlaySong = () => {
const {setCurrentSong} = useContext(StoreContext);
useEffect( () => {
setCurrentSong("some song");
}, []
}
return null;
}
const MyComponent = () => {
const [shouldPlay, setShouldPlay] = useState(false);
return (
<>
<button onClick={() => setShouldPlay(true)}>Play</button>
{shouldPlay && <PlaySong />}
</>
)
}
I'd like to open a materialui dialog and handle the result from javascript to make a simple Yes/no prompt.
Id like it to work something like this (just mockup code to explain)
<MyPromptComponent />
{
MyPromptComponent.show('Do you really want to?').then((result) => alert(result ? 'then do it' : 'walk away') );
}
So the question is; How (if) can I put functions in my component, that I can call from a reference?
If someone knows of an example where something similar is dont I'd appreciate it.
Edit : 11/10/2020
The "problem" with the state way of doing this is that I have to leave the function showing the prompt, having to store temporary variables outside the function. If I could do something like this the code would be much more readable:
{
let tempData = doAProcessForThisFunctionOnly();
let sureResult = confirmDialog.show('Are you sure?');
if(sureResult )
doSomeMoreWithTempData(tempData);
else
doSomeOtherStuff(tempData);
doSomeEndStuff(tempdata);
}
In react I have to do this
{
let tempData = doAProcessForThisFunctionOnly();
tempDataRef.current = tempData;
setShowDialog();
}
onYes = () => {
let workData = tempDataRef.current;
doSomeMoreWithTempData(workData );
doSomeEndStuff(workData)
}
onNo = () => {
let workData = tempDataRef.current;
doSomeOtherStuff(workData );
doSomeEndStuff(workData)
}
doSomeEndStuff = (workData) => {
//Do the stuff here
}
It really seems I need to jump in and out of a lot of functions just to get a simple confirmation and even using variables outside the functon (refs).
That really seems a big step backwards code-wise, to me.
The "vanilla" way of doing this would even let me use the same prompt-dialog component from many different functions. In reactit seems I need a separate confirm-dialog for each case as the "yes"/"no" events are hardcoded per case.
You should control everything with state:
export default function App() {
const [show, setShow] = useState(false);
return (
<div className="App">
<button onClick={() => setShow(true)}>Initiate</button>
<MyPromptComponent
title="Do you really want to?"
show={show}
onConfirm={() => {
setShow(false);
alert("Then do it")
}}
onCancel={() => {
setShow(false);
alert("Walk away")
}}
/>
</div>
);
}
const MyPromptComponent = ({ show, title, onConfirm, onCancel }) => {
return (
<React.Fragment>
{show &&
<div>
Lets pretend this is modal - {title}
<button onClick={() => onConfirm()}>Confirm</button>
<button onClick={() => onCancel()}>Cancel</button>
</div>
}
</React.Fragment>
);
};
Please see sandbox
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>
);
};
When passing callback to component, I should use useCallback hook to return a memoized callback (to prevent unneeded renders):
import doSomething from "./doSomething";
const FrequentlyRerenders = ({ id }) => {
const onEvent = useCallback(() => doSomething(id), [id]);
return (
<ExpensiveComponent onEvent={ onEvent } />
);
};
But what if I am using map? for example:
import doSomething from "./doSomething";
const FrequentlyRerendersMap = ({ ids }) => {
return ids.map(id => {
const onEvent = useCallback(() => doSomething(id), [id]);
return (
<ExpensiveComponent key={id} onEvent={ onEvent } />
);
});
};
How should I properly use useCallback? Is the above the right way to pass multiple callbacks? Is it really works and know to memioze every callback according to an item of an array?
Convert the returned mapped JSX into a component and then you can useCallback without problems
import doSomething from "./doSomething";
const MappedComponent =(props) => {
const onEvent = useCallback(() => doSomething(props.id), []);
return (
<ExpensiveComponent onEvent={ onEvent } />
);
}
const FrequentlyRerendersMap = ({ ids }) => {
return ids.map(id => {
return <MappedComponent key={id} id={id} />
});
};
This is now expressly discouraged in the docs.
https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
Old answer
The answer to re architect is sidestepping the question IMO. Though, I think creating a new component is probably a good idea.
To answer the question though, your code:
import doSomething from "./doSomething";
const FrequentlyRerendersMap = ({ ids }) => {
return ids.map(id => {
const onEvent = useCallback(() => doSomething(id), [id]);
return (
<ExpensiveComponent key={id} onEvent={ onEvent } />
);
});
};
Is actually what you would want to do to memoize in a map. I don't know the implementation of useCallback, but it should add very little memory overhead. A stackFrame, and whatever they do to reduce the array into some kind of key for the memoized function.
Unless your working on some with a TON of elements, EG react virtualized components unlimited scrolling, you are realistically safe to useCallback the way you are. In fact, the small memory overhead is probably a much cheaper price than the rerender of all of those components.