Skip to content
Advertisement

State from react useState is updated when key property is used but requires useEffect or similar method to update otherwise

I have the following component:

import React, { useState } from "react";
import { FormControl, TextField } from "@material-ui/core";

interface IProps {
    text?: string;
    id: number;
    onValueChange: (text: string, id: number) => void;
    placeholder: string;
}

export const QuestionTextRow: React.FC<IProps> = (props) => {
    const [item, onItemChange] = useState(props.text);

    const onChange = (e: React.FormEvent<HTMLInputElement>) => {
        const newValue = e.currentTarget.value;

        onItemChange(newValue);
        props.onValueChange(newValue, props.id);
    };

    return (
        <>
            <FormControl fullWidth>
                <div>{props.text}</div>
                <TextField
                    aria-label="question-text-row"
                    onDragStart={(e) => {
                        e.preventDefault();
                        e.stopPropagation();
                    }}
                    value={item}
                    onChange={(ev: React.ChangeEvent<HTMLInputElement>): void => {
                        onChange(ev);
                    }}
                />
            </FormControl>
        </>
    );
};

It is rendered via the following component:

const renderQuestionOptions = (id: number): JSX.Element => {
    const item = props.bases.find((x) => x.sortableId === id);

    if (!item) return <> </>;

    return (
        <div className={classes.questionPremiseRow}>
            <div className={classes.rowOutline}>
                <QuestionOptionsSortableRow item={item} isDisabled={false} onClickRow={onClickBasisRow}>
                    <QuestionTextRow
                        text={item.text ? item.text.text : ""}
                        id={item.sortableId}
                        onValueChange={basisValueChanged}
                        placeholder={intl.formatMessage({ id: "question.create.basis.row.placeholder" })}
                    ></QuestionTextRow>
                </QuestionOptionsSortableRow>
            </div>
        </div>
    );
};

It renders the following list:

enter image description here

As you can see props.text and useState item from props.text are rendered equally. If props.text is updated it does not reflect on useState though.

https://stackoverflow.com/a/53846698/3850405

enter image description here

enter image description here

I can solve it by useEffect to make it work:

useEffect(() => {
    onItemChange(props.text);
}, [props.text]);

https://reactjs.org/docs/hooks-effect.html

https://stackoverflow.com/a/54866051/3850405

However If I add key={`${item.text?.text}-${item.sortableId}`} to QuestionTextRow it will work without using useEffect. How come?

I know a static unique key should be used but would it not be the same result if key={item.uniqueId} was used?

https://www.npmjs.com/package/react-key-index

The argument passed to useState is the initial state much like setting state in constructor for a class component and isn’t used to update the state on re-render

https://stackoverflow.com/a/43892905/3850405

Advertisement

Answer

However If I add key={${item.text?.text}-${item.sortableId}} to QuestionTextRow it will work without using useEffect. How come?

That is because of reconciliation. In react, when on one render you have say:

<SomeComponent key={1}/>

If on next render you render same component (at the same place) with different key, say:

<SomeComponent key={2}/>

React will destroy instance related to previous component and create a new instance for this one, hence the useState inside that component will be initialized with the provided text property once again (like when you created the component first time).

If the key is same for some component on previous and next renders and you just change some other props, in this case the component is re-rendered (no instance destroyed), that’s why you didn’t see the text property reflected in state.


Sometimes it can be tricky to copy props to state like you have in your useEffect solution, I recommend you read this post, it is about classes but same ideas apply.

User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement