Wanna get a room full of React engineers talking? Mention state management.

Everyone has their favorite library. The Redux camp loves their actions and reducers and sagas and whatnot. The MobX camp loves observers and actions and overwritten object defaults and magic.

Both say their way is simplest. Both hide many footguns.

https://twitter.com/thekitze/status/1085062217312153600

It’s 2019. You probably don’t need a state management library.

🤔

Got a simple app? A handful of properties shared between a couple components?

Use local state.

Local state is your best friend

Contrary to popular belief, you don’t need complex state management to build a login form with two fields. Something like this will do 👇

class Field extends React.Component {
    state = {
        value: "",
        error: ""
    }
    onChange = event => this.setState({
        value: event.target.value
    }, this.validate);
    validate = () => {
        const { value } = this.state;
        if (!someValidationRequirement(value)) {
            this.setState({
                error: "My lovely error"
            })
        }
    }
    render() {
        const { value, error } = this.state;
        return (
            <>
                <input value={value} onChange={this.onChange} />
                {error}
            </>
        )
    }
}

That’s a field with a value and error state. Typing triggers the onChange handler and updates state. This triggers a re-render and users can see what they’re typing.

Field validation triggers after state update, checks against some external function, and sets the error. You could use the presence of this error to make your field red or whatever.

A login form can reuse this field twice.

class Login extends React.Component {
    render() {
        return (
            <div>
                <Field />
                <Field />
            </div>
        )
    }
}

Hoisted state is best state

“But Swizec, how will my form get those values? How will joint validation work?”

Right. It’s best to hoist your state into the form. You’ve just discovered that the form cares about this state, not the fields.

The magic of asking questions to avoid building things you don’t need.

Something like this, then 👇

const Field = ({ onChange, value, error }) => (
    <>
        <input value={value} onChange={onChange} />
        {error}
    </>
)
class Login extends React.Component {
    state = {
        user: {
            value: "",
            error: ""
        },
        pass: {
            value: "",
            error: ""
        },
        error: ""
    }
    validate = () => {
        const { user, pass } = this.state;
        if (!someValidation(user.value) || !someValidation(pass.value)) {
            this.setState({
                error: "A lovely form error"
            });
        }
    }
    onChangeUser = event => this.setState({
        user: {
            value: event.target.value
        }
    }, () => {
        if (!someValidation(event.target.value)) {
            this.setState({
                user: {
                    value: event.target.value,
                    error: "A user error oh my"
                }
            })
        }
    }
    onChangePass = event => this.setState({
        pass: {
            value: event.target.value
        }
    }, () => {
        if (!someValidation(event.target.value)) {
            this.setState({
                pass: {
                    value: event.target.value,
                    error: "A password error oh my"
                }
            })
        }
    }
    render() {
        const { user, pass, error } = this.state;
        return (
            <form>
                <Field value={user.value} error={user.error} onChange={this.onChangeUser} />
                <Field value={pass.value} error={pass.error} onChange={this.onChangePass} />
                {error}
            </form>
        )
    }
}

Pretty repetitive, I agree. But it works and you didn’t need no library.

Same concept as before except we moved all the Field machinery into our form. Duplicated it twice for two fields and turned fields into fully managed components.

You can just imagine how this code explodes in complexity the more fields you add.

Context to the rescue

We can think of forms as sections of our app. Contexts.

The modern context API makes it pretty painless to create ad-hoc contexts that share state among several components. We can use that to move some logic back into our fields.

const FormContext = React.createContext();

This creates a FormContext We’re using it in a contrived way and I’ll explain why.

class Field extends React.Component {
    state = {
        value: this.props.value,
        error: ""
    }
    onChange = event => this.setState({
        value: event.target.value
    }, this.validate);
    validate = () => {
        const { value } = this.state;
        if (!someValidationRequirement(value)) {
            this.setState({
                error: "My lovely error"
            })
        } else {
            this.props.returnValue(this.state.value)
        }
    }
    render() {
        const { value, error } = this.state;
        return (
            <>
                <input value={value} onChange={this.onChange} />
                {error}
            </>
        )
    }
}
const ContextField = ({ name }) => (
    <FormContext.Consumer>
        {(state) => (
            <Field value={name} returnValue={value => state.onChange(value, name) />
        )}
    </FormContext>
)<br>

Same smart form field as we had before. It takes care of validation and temp value keeping internally. When it’s happy it returns the verified value to the form with the this.props.returnValue method.

The returnValue method and initial field value come from context. That’s where the ContextField component comes in.

With a shared context we can render these fields as deep inside our tree as we want, a different file even, and they can all talk to our Login form.

The Login form then looks like this:

class Login extends React.Component {
    state = {
        user: "",
        pass: "",
        error: "",
        onChange: (value, field) => this.setState({
            [field]: value
        }, this.validate)
    }
    validate = () => {
        const { user, pass } = this.state;
        if (!someValidation(user) || !someValidation(pass)) {
            this.setState({
                error: "A lovely form error"
            });
        }
    }
    render() {
        const { error } = this.state;
        return (
            <FormContext.Provider value={this.state}>
                <ContextField name="user" />
                <ContextField name="pass" />
                {error}
            </FormContext.Provider>
        )
    }
}

We now have a form that keeps valid user and pass state, provides a change method via context, and renders fields by just giving them a name.

With this approach you can add as many fields as you want with very little overhead. You could make further improvements by thinking up types of fields, passing validations with props etc.

But there’s still a lot of code to look at.

State with hooks, oh my ❤️

That’s where hooks come in. Hooks make stuff like this a breeze.

Check out that same field implemented with React hooks 👇

const Field = ({ value, returnValue }) => {
    const [state, setState] = useState(value);
    const [error, setError] = useState("");
    useEffect(() => {
        if (!someValidationRequirement(state)) {
            setError("My lovely error");
        } else {
            returnValue(state)
        }
    }, [state])
    return (
        <>
            <input value={state} onChange={event => setState(event.target.value)} />
        </>
    )
}

useState creates convenient state management. A getter and a setter. First const is the value, second const sets the value.

useEffect runs our validation method on every change of state. It’s that second argument that ensures our function runs on changes only.

You can replicate the rest of our setup with useContext. You’ll wind up with similar logical complexity and much less code.

Constate makes useContext great 👌

Rather than messing around with useContext, I suggest using Constate. It was already my favorite library a few months ago when it was just for modern React Context.

[https://www.youtube.com/watch?v=63UI2nTz1qA]

With the change to hooks it became truly amaze.

Something like this 👇

function useForm() {
    const [user, setuser] = useState("");
    const [pass, setpass] = useState("");
    const [error, setError] = useState("");
    return { user, pass, error, setUser, setPass, setError };
}
const FormContainer = createContainer(userForm);
const ContextField = ({ name }) => (
    const state = useContext(FormContainer.Context);
    return <Field value={state[name]} returnValue={state[`set${name}`} />
)
function Error() {
    const { error, setError, user, pass } = useContext(FormContainer.Context);
    useEffect(() => {
        if (!someValidation(user) || !someValidation(pass)) {
            setError("A lovely form error")
        }
    }, [user, pass])
    return (
        {error}
    )
}
function Login() {
    <ContextField.Provider>
        <ContextField name="user" />
        <ContextField name="pass" />
        <Error />
    </ContextField.Provider>
}

😱

Beautiful!

Here’s how it works:

  1. Custom hook combines all the state handling we need.
  2. Our hook returns its external API as an object.
  3. We use Constate’s createContainer method to wrap it in a container
  4. Like before, ContextField is our context-based wrapper. It takes a name and uses it to get initial value and returnValue API from state. In this case it dynamically decides which setX method to use.
  5. Error is a new method. Because of context, we can extract form error handling into a new component. Once more useEffect runs our validations when user or pass change.
  6. The Login form is now our simplest component. Renders context, fields, and error.

Delightful state management with hooks and Constate. QED

🙂

Learned something new? Want to improve your skills?

Join over 10,000 engineers just like you already improving their skills!

Here's how it works 👇

Leave your email and I'll send you an Interactive Modern JavaScript Cheatsheet 📖right away. After that you'll get thoughtfully written emails every week about React, JavaScript, and your career. Lessons learned over my 20 years in the industry working with companies ranging from tiny startups to Fortune5 behemoths.

PS: You should also follow me on twitter 👉 here.
It's where I go to shoot the shit about programming.