Wednesday, 8 January 2020

Which of these strategies is the best way to reset a component's state when the props change

I have a very simple component with a text field and a button:

<image>

It takes a list as input and allows the user to cycle through the list.

The component has the following code:

import * as React from "react";
import {Button} from "@material-ui/core";

interface Props {
    names: string[]
}
interface State {
    currentNameIndex: number
}

export class NameCarousel extends React.Component<Props, State> {

    constructor(props: Props) {
        super(props);
        this.state = { currentNameIndex: 0}
    }

    render() {
        const name = this.props.names[this.state.currentNameIndex].toUpperCase()
        return (
            <div>
                {name}
                <Button onClick={this.nextName.bind(this)}>Next</Button>
            </div>
        )
    }

    private nextName(): void {
        this.setState( (state, props) => {
            return {
                currentNameIndex: (state.currentNameIndex + 1) % props.names.length
            }
        })
    }
}

This component works great, except I have not handled the case when the state changes. When the state changes, I would like to reset the currentNameIndex to zero.

What is the best way to do this?


Options I have conciderred:

Using componentDidUpdate

This solution is ackward, because componentDidUpdate runs after render, so I need to add a clause in the render method to "do nothing" while the component is in an invalid state, if I am not careful, I can cause a null-pointer-exception.

I have included an implementation of this below.

Using getDerivedStateFromProps

The getDerivedStateFromProps method is static and the signature only gives you access to the current state and next props. This is a problem because you cannot tell if the props have changed. As a result, this forces you to copy the props into the state so that you can check if they are the same.

Making the component "fully controlled"

I don't want to do this. This component should privately own what the currently selected index is.

Making the component "fully uncontrolled with a key"

I am considering this approach, but don't like how it causes the parent to need to understand the implementation details of the child.

Link


Misc

I have spent a great deal of time reading You Probably Don't Need Derived State but am largely unhappy with the solutions proposed there.

I know that variations of this question have been asked multiple times, but I don't feel like any of the answers weigh the possible solutions. Some examples of duplicates:


Appendix

Solution using componetDidUpdate (see description above)

import * as React from "react";
import {Button} from "@material-ui/core";

interface Props {
    names: string[]
}
interface State {
    currentNameIndex: number
}

export class NameCarousel extends React.Component<Props, State> {

    constructor(props: Props) {
        super(props);
        this.state = { currentNameIndex: 0}
    }

    render() {

        if(this.state.currentNameIndex >= this.props.names.length){
            return "Cannot render the component - after compoonentDidUpdate runs, everything will be fixed"
        }

        const name = this.props.names[this.state.currentNameIndex].toUpperCase()
        return (
            <div>
                {name}
                <Button onClick={this.nextName.bind(this)}>Next</Button>
            </div>
        )
    }

    private nextName(): void {
        this.setState( (state, props) => {
            return {
                currentNameIndex: (state.currentNameIndex + 1) % props.names.length
            }
        })
    }

    componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>): void {
        if(prevProps.names !== this.props.names){
            this.setState({
                currentNameIndex: 0
            })
        }
    }

}

Solution using getDerivedStateFromProps:

import * as React from "react";
import {Button} from "@material-ui/core";

interface Props {
    names: string[]
}
interface State {
    currentNameIndex: number
    copyOfProps?: Props
}

export class NameCarousel extends React.Component<Props, State> {

    constructor(props: Props) {
        super(props);
        this.state = { currentNameIndex: 0}
    }

    render() {

        const name = this.props.names[this.state.currentNameIndex].toUpperCase()
        return (
            <div>
                {name}
                <Button onClick={this.nextName.bind(this)}>Next</Button>
            </div>
        )
    }


    static getDerivedStateFromProps(props: Props, state: State): Partial<State> {

        if( state.copyOfProps && props.names !== state.copyOfProps.names){
            return {
                currentNameIndex: 0,
                copyOfProps: props
            }
        }

        return {
            copyOfProps: props
        }
    }

    private nextName(): void {
        this.setState( (state, props) => {
            return {
                currentNameIndex: (state.currentNameIndex + 1) % props.names.length
            }
        })
    }


}


from Which of these strategies is the best way to reset a component's state when the props change

No comments:

Post a Comment