is it possible to use a function as my React Component's state ?
example code here:
// typescript
type OoopsFunction = () => void;
export function App() {
const [ooops, setOoops] = React.useState<OoopsFunction>(
() => console.log('default ooops')
);
return (
<div>
<div onClick={ ooops }>
Show Ooops
</div>
<div onClick={() => {
setOoops(() => console.log('other ooops'))
}}>
change oops
</div>
</div>
)
}
but it doesn't works ... the defaultOoops will be invoked at very beginning, and when clicking change oops, the otrher ooops will be logged to console immediately not logging after clicking Show Ooops again.
why ?
is it possible for me to use a function as my component's state ?
or else React has its special ways to process such the function state ?
It is possible to set a function in state using hooks, but because state can be initialized and updated with a function that returns the initial state or the updated state, you need to supply a function that in turn returns the function you want to put in state.
const { useState } = React;
function App() {
const [ooops, setOoops] = useState(() => () => console.log("default ooops"));
return (
<div>
<button onClick={ooops}>Show Ooops</button>
<button
onClick={() => {
setOoops(() => () => console.log("other ooops"));
}}
>
change oops
</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
TL;DR
Yes, it is possible to use a function as the React Component's state. In order to do this, you must use a function returning your function in React.useState:
const [ooops, setOoops] = React.useState<OoopsFunction>(
() => () => console.log('default ooops')
);
// or
const yourFunction = () => console.log('default ooops');
const [ooops, setOoops] = React.useState<OoopsFunction>(
() => yourFunction
);
To update your function you also must use a function returning your function:
setOoops(() => () => console.log("other ooops"));
// or
const otherFunction = () => console.log("other ooops");
setOoops(() => otherFunction);
Detailed Answer
Notes about React.useState
The signature of useState in React with types is
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
It shows, that there are two ways to set the initial value in your state:
Provide the initial value as is (React.useState(0) - initial value 0),
Provide a function, which returns the initial value to be set (React.useState(() => 0) - initial value also 0).
Important to note: If you provide a function in React.useState, then this function is executed, when React.useState is executed and the returned value is stored as the initial state.
How to actually store functions
The problem here is if you want to store a function as state you can not provide it as initial state as is, because this results in the function being executed and its return value stored as state instead of the function itself. Therefore when you write
const [ooops, setOoops] = React.useState<OoopsFunction>(
() => console.log('default ooops')
);
'default ooops' is logged immediately when React.useState is called and the return value (in this case undefined) is stored.
This can be avoided by providing a higher order function returning your function you want to store:
const [ooops, setOoops] = React.useState<OoopsFunction>(
() => () => console.log('default ooops')
);
This way the outer function will be definitely executed when first running React.useState and its return value will be stored. Since this return value is now your required function, this function will be stored.
Notes about the state setter function
The state setter function's (here setOoops) signature is given as
Dispatch<SetStateAction<S>>
with
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);
Like in React.useState there is also the possibility to update state with a value or a function returning the value. So in order to update state the higher order function from above has to be used as well.
The previous answers set me on the right track, but I needed to tweak my setState parameters slightly as I wanted to execute a function complete with parameters. The following worked for me!
const [handler, setHandler] = useState(() => {});
setHandler(() => () => enableScheduleById(scheduleId));
Related
I want to change State with child elements in React. However, when I click once, it is not immediately updated. Click twice, it shows the correct answer.
How to update async?
export default function Example() {
const onClick = async () => {
console.log('a', test)
// should be 'b', but console log 'a'
}
const [test, setTest] = useState('a')
return (
<ClickExample setTest={setTest} onClick={onClick} />
)
}
export default function ClickExample() {
const next = useCallback(
(alphabet: string) => {
setTest(alphabet)
onClick()
},
[onClick, setTest],
)
return <SelectButton onClick={() => next('b')} />
}
You can receive the value to be updated as an argument from the onClick callback. It'll be something like this:
export default function Example() {
const [test, setTest] = useState('a')
const handleClick = (newValue) => {
setTest(newValue);
}
return (
<ClickExample onClick={handleClick} />
)
}
export default function ClickExample({ onClick }) {
return <SelectButton onClick={() => onClick('b')} />
}
NOTE: You should avoid using useCallback() when it is not necessary. Read more over the web but this article from Kent C. Dodds is a good start. As a rule of thumb: Never use useCallback()/useMemo() unless you REALLY want to improve performance after needing that improvement.
In the first render, the value of test is equal to'a'. So when the console.log is executed, it has already captured 'a' as the value of test state. (See closures and stale closures).
One way to fix this would be to create a handleClick function in the parent component which receives the new value of test as its input and set the state and log the new value(which will be updated in the next render) using its argument.
// ClickExample
const handleClick = (alphabet) => {
setTest(alphabet);
console.log('a', alphabet);
};
codesandbox
Having tried useState, it's functiional variation and attempt with useEffect (not allowed in a callback function).
I am so stuck.
I have a parent 'Table' component, which renders a TableHead child and a TableBody child.
The TableHead child has a checkbox, which when clicked executes a callback function on the parent.
At this point the boolean selectAll (from a useState setter and value), is supposed to toggle (change value).
But it remains in it's initial state.
the result is that the first time the header checkbox for selectall, does fire and the re-render does show all the rows in the body as checked, but then unchecking the 'selectAll' does fire the callback, but the 'selectAll' remains false and all the rows remain checked.
Parent component Code:
function OTable(props) {
const [selectAll, setSelectAll] = useState(false);
const onAllRowsSelected = useCallback(() => {
if (selectAll === false)
{
setSelectAll(selectAll => selectAll = true);
}
else
{
setSelectAll(selectAll => selectAll = false);
}
}
return (
<TableContainer>
<Table>
<OTableHead
onSelectAllClick={onAllRowsSelected}
setSelectAll={setSelectAll}
/>
<OTableBody
selectAll={selectAll}
/>
</Table>
</TableContainer>
How to do it?
Thanks
If I make a couple of reasonable assumptions (for instance, that you have the closing ) on the useCallback call), your code for toggling selectAll works, though from your use of useCallback I suspect it doesn't quite work the way you want it to. Here's your code with those assumptions:
const {useState, useCallback} = React;
const TableContainer = ({children}) => {
return <div>{children}</div>;
};
const Table = ({children}) => {
return <div>{children}</div>;
};
const OTableHead = ({onSelectAllClick, children}) => {
console.log(`OTableHead is rendering`);
return <div>
<input type="button" onClick={onSelectAllClick} value="Select All" />
<div>{children}</div>
</div>;
};
const OTableBody = ({selectAll}) => {
return <div>selectAll = {String(selectAll)}</div>;
};
function OTable(props) {
const [selectAll, setSelectAll] = useState(false);
const onAllRowsSelected = useCallback(() => {
if (selectAll === false)
{
setSelectAll(selectAll => selectAll = true);
}
else
{
setSelectAll(selectAll => selectAll = false);
}
});
return (
<TableContainer>
<Table>
<OTableHead
onSelectAllClick={onAllRowsSelected}
setSelectAll={setSelectAll}
/>
<OTableBody
selectAll={selectAll}
/>
</Table>
</TableContainer>
);
}
ReactDOM.render(
<OTable />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
But some things stand out:
You're not passing any dependency array to useCallback. That doesn't do anything useful, because useCallback will always return the new function you pass it. I suspect you meant to have an empty dependency array on it so that it always reused the first function (to avoid unnecessary re-rendering of OTableHead).
You're using the callback form of setSelectAll, but you're using hardcoded values (true and false). This code:
const onAllRowsSelected = useCallback(() => {
if (selectAll === false)
{
setSelectAll(selectAll => selectAll = true);
}
else
{
setSelectAll(selectAll => selectAll = false);
}
});
does exactly what this code would do (given that we know that selectAll is a boolean to start with, it would be very subtly different if we didn't know that):
const onAllRowsSelected = useCallback(() => {
setSelectAll(!selectAll);
});
because the if uses the version of selectAll that the function closes over, not the parameter the callback received. (setSelectAll(selectAll => selectAll = false); is functionally identical to setSelectAll(() => false), assigning to the parameter doesn't have any effect.) And in turn, that code is the same as this:
const onAllRowsSelected = () => {
setSelectAll(!selectAll);
};
But I suspect you used the callback version for the same reason you used useCallback.
The code doesn't succeed in avoiding having the re-rendering, as you can see from the console.log I added to OTableHead above.
useCallback is useful for avoiding making child elements re-render if the callback hasn't really changed, by memoizing the callback. Here's how you'd use it correctly in that code
Pass an empty dependencies array to useCallback so it only ever returns the first callback you define.
Use the parameter value that the function version of setSelectAll passes your callback.
Ensure that the component you want to have not re-render if the callback didn't change implements checks on its properties and doesn't re-render when they haven't changed. With a function component like OTableHead you can do that just by passing it through React.memo.
Here's the example above with those changes:
const {useState, useCallback} = React;
const TableContainer = ({children}) => {
return <div>{children}</div>;
};
const Table = ({children}) => {
return <div>{children}</div>;
};
// *** Use `React.memo`:
const OTableHead = React.memo(({onSelectAllClick, children}) => {
console.log(`OTableHead is rendering`);
return <div>
<input type="button" onClick={onSelectAllClick} value="Select All" />
<div>{children}</div>
</div>;
});
const OTableBody = ({selectAll}) => {
return <div>selectAll = {String(selectAll)}</div>;
};
function OTable(props) {
const [selectAll, setSelectAll] = useState(false);
const onAllRowsSelected = useCallback(() => {
// Callback version, using the parameter value
setSelectAll(selectAll => !selectAll);
}, []); // <=== Empty dependency array
return (
<TableContainer>
<Table>
<OTableHead
onSelectAllClick={onAllRowsSelected}
setSelectAll={setSelectAll}
/>
<OTableBody
selectAll={selectAll}
/>
</Table>
</TableContainer>
);
}
ReactDOM.render(
<OTable />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
If you aren't worried about unnecessary re-rendering, then you can get rid of useCallback entirely and just do this:
const onAllRowsSelected = () => {
setSelectAll(!selectAll);
};
You don't need useCalback in this case. Just update setState like this:
const onAllRowsSelected = () => {
setSelectAll((preState) => !preState);
};
Yes, keeps the inital value because useCallback is a memoization and, if you don't add the state dependencies, it keeps the initial value (due to the memoization itself). To solve, just put selectAll as useCallback dependencies:
const onAllRowsSelected = useCallback(() => {
setSelectAll((prev) => !prev)
}, [selectAll])
There is no need to memoize the state updater function since React guarantees it to be a stable reference.
useState
Note
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
You can simplify your callback to just update the state using the functional update.
const onAllRowsSelected = () => setSelectAll(all => !all);
If OTableHead requires that onSelectAllClick prop be a stable reference, then the useCallback can be used with an empty dependency array in order to provide a stable onAllRowsSelected callback reference. Note: this doesn't effect the ability for onAllRowsSelected to correctly toggle from the previous state value, it's only to provide a stable callback reference to children components.
useCallback
useCallback will return a memoized version of the callback that only
changes if one of the dependencies has changed. This is useful when
passing callbacks to optimized child components that rely on reference
equality to prevent unnecessary renders (e.g.
shouldComponentUpdate).
const onAllRowsSelected = useCallback(
() => setSelectAll(all => !all),
[],
);
I have this, every time addEmail is called, the updater callback for setEmails is called twice and therefore the new email is added twice:
const Container = (props: Props) => {
const [emails, setEmails] = useState<Array<string>>([]);
const addEmail = (email: string) => {
setEmails((prevState: string[]) => [...prevState, email]);
};
return (
<Wrapper>
{emails.map((email, index) => {
return (
<Email
key={index}
text={email}
onRemoveClicked={() => {}}
/>
);
})}
<Input onSubmit={addEmail} />
</Wrapper>
);
};
How am i supposed to do this on strict mode ?
You need to provide a lot more context here, This component doesn't look like a native html form. Email and Input both are custom components.
I would suggest you to try the below steps to debug this
Check if Input component is calling onSubmit twice.
React's useState setter method takes the new state as argument, you are passing a function here, that might be the problem. Try changing the addEmail function definition to below and see if it works
const addEmail = (newEmail: string) => setEmails([...emails, newEmail]);
Read More about React state hook.
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>
);
};
I am building a table component. It gets as a prop an object called content which holds records that are displayed as the table's content. The component has a state called 'currentRecord' which holds the id of the selected row (changes in onClick event on each row).
I want to set the first record's id to be the initial state using the 'useState'.
As an initial state argument for the 'useState' it has a function which return the key(which is the id) of the first record in the content prop. But it returns undefined. When console logging the return value of that function, it return an id.
Why does it return undefined when setting the initial state using a function?
I have tried setting the initial state using a string instead of a function, and it worked.
function getFirstOrderId(content:object): string {
return Object.keys(content)[0];
}
const Table: FunctionComponent<Props> = props => {
const { columnTitles, content, onRowClick } = props;
const [currentRecord, setCurrentRecord] = useState(getFirstOrderId(content));
useEffect(() => {
onRowClick(currentRecord);
}, [currentRecord]);
return (
<StyledTable>
<thead>
<tr>
{Object.values(columnTitles).map(fieldName => {
return <th>{fieldName}</th>;
})}
</tr>
</thead>
<StyledTBody>
{mapWithKeys((order: any, id: string) => {
return (
<StyledRow
key={id}
isSelected={id === currentRecord}
onClick={() => setCurrentRecord(id)}
onDoubleClick={() => window.open("/" + order)}
>
{Object.keys(columnTitles).map(fieldContent => {
return <td>{order[fieldContent]}</td>;
})}
</StyledRow>
);
}, content)}
</StyledTBody>
</StyledTable>
);
};
export default Table;
Put a function inside the useState hook and return the value.
const [value, setValue] = useState(() => ({key: "Param"}));
console.log(value) // output >> {key: "Param"}
This might work:
const [currentRecord, setCurrentRecord] = useState(null);
useEffect(()=>{ // This will run after 1st render
setCurrentRecord(getFirstOrderId(content)); // OPTION 1
setCurrentRecord(()=>{ // OPTION 2
return getFirstOrderId(content);
});
},[]);
You can set up a loading state to wait for the useEffect() to take place.
You can actually do lazy initialisation to state with a function. how ever you called the function and not passed in as a parameter, meaning you passed the returned value of the function as the initial value to use state.
You can check out the official explanation:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all