Search code examples
javascriptreactjstypescriptrefactoringreact-lifecycle

How to use getDerivedStateFromProps in an update state situation?


I'm confused about the refactoring of my code.

My Component works like this ==> I set the incoming props from the parent component to the child component's state. Then I can do the update state operations in the component.

This is my UNSAFE_componentWillReceiveProps function:

UNSAFE_componentWillReceiveProps = (nextProps) => {
    if (nextProps 
        && nextProps.actionToEdit 
        && nextProps.actionToEdit.statement
    ) {
        const regex = /@\w+/g;
        const found = nextProps.actionToEdit.statement.match(regex);
        this.setState({
            parameters: found
        });
    }
};

This is my getDerivedStateFromProps function:

static getDerivedStateFromProps(
    nextProps: ICustomActionModalProps, 
    prevState: ICustomActionModalState
) {
    if (nextProps 
        && nextProps.actionToEdit 
        && nextProps.actionToEdit.statement
    ) {
        const regex = /@\w+/g;
        const found = nextProps.actionToEdit.statement.match(regex);
        return {
            parameters: found
        };
    }
    return null; 
}

This is my onChange function:

onChange = (newValue, e) => {
    const regex = /@\w+/g;
    const found = newValue.match(regex);
    this.setState({ parameters: found });
};

I realized something when onChange worked.

==> If I use UNSAFE_componentWillReceiveProps, the state updated perfectly. Because when the onChange function works every time UNSAFE_componentWillReceiveProps doesn't work.

However,

==> If I use getDerivedStateFromProps, the state updated with old props. Because when the onChange function works every time getDerivedStateFromProps works too.

I just want my getDerivedStateFromProps function will able to works like my old UNSAFE_componentWillReceiveProps function.

How can I do that? Thanks


Solution

  • The problem you're describing is solved either by making your Component fully controlled or fully uncontrolled but with a key.

    The first approach may look like this:

    // --- parent component: Parent.tsx
    import { Child } from "./Child";
    
    export class Parent extends React.Component<{}, { counter: number }> {
      state = { counter: 0 };
    
      render() {
        return (
          <div>
            <div>
              <button onClick={() => this.setState(({ counter }) => ({ counter: counter + 1 }))}>increase counter</button>
            </div>
            <Child
              parentCounter={this.state.counter}
              setParentCounter={(n) => this.setState({ counter: n })}
            />
          </div>
        );
      }
    }
    
    // --- child component : Child.tsx
    type Props = { parentCounter: number; setParentCounter: (n: number) => void };
    
    export class Child extends React.Component<Props> {
      render() {
        const { parentCounter, setParentCounter } = this.props;
    
        return (
          <>
            <div>parent counter: {parentCounter}</div>
            <button
              onClick={() => {
                setParentCounter(parentCounter + 1);
              }}
            >
              increase parent counter
            </button>
          </>
        );
      }
    }
    

    So, there is not two separate states: one in the parent component and another in the child one. The only state exists in the parent component and child component has the setter prop it may use to change parent's state.

    The second approach (uncontrolled component with a key) uses the fact that when the key changes the child component rerenders from scratch and looses it's inner state:

    // --- Parent.tsx
    import { Child } from "./Child";
    
    export class Parent extends React.Component<{}, { counter: number }> {
      state = { counter: 0 };
    
      render() {
        const { counter } = this.state;
    
        return (
          <div>
            <div>parent counter: {counter}</div>
            <div>
              <button
                onClick={() =>
                  this.setState(({ counter }) => ({ counter: counter + 1 }))
                }
              >
                increase parent counter
              </button>
            </div>
            <Child parentCounter={counter} key={`child-key-${counter}`} />
          </div>
        );
      }
    }
    
    // --- Child.tsx
    type Props = { parentCounter: number };
    type State = { childCounter: number };
    
    export class Child extends React.Component<Props, State> {
      constructor(props: Props) {
        super(props);
    
        this.state = { childCounter: props.parentCounter };
      }
    
      render() {
        return (
          <>
            <div>child counter {this.state.childCounter}</div>
            <button
              onClick={() => {
                this.setState(({ childCounter }) => ({
                  childCounter: childCounter + 1
                }));
              }}
            >
              increase child counter
            </button>
          </>
        );
      }
    }