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.
lmfao I just opened the code for https://t.co/Yl2Sb0obhb and I've used redux and redux-form for *ONE* search field. Back then I was falling for these hype traps like a fucking blind sheep.
— kitze 🚀 (@thekitze) January 15, 2019
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></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 class="consumer">
{(state) => (
<Field value={name} returnValue={value ==""} state.onChange(value, name) />
)}
</FormContext>
)
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 class="provider" value={this.state}>
<ContextField name="user">
<ContextField name="pass">{error}</ContextField>
</ContextField>
</FormContext>
)
}
}
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.
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 class="provider" />
<ContextField name="user" />
<ContextField name="pass"/>
<Error />
}
😱
Beautiful!
Here's how it works:
- Custom hook combines all the state handling we need.
- Our hook returns its external API as an object.
- We use Constate's
createContainer
method to wrap it in a container - 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. Error
is a new method. Because of context, we can extract form error handling into a new component. Once moreuseEffect
runs our validations whenuser
orpass
change.- The
Login
form is now our simplest component. Renders context, fields, and error.
Delightful state management with hooks and Constate. QED
:)
Continue reading about Delightful state management with hooks and Constate
Semantically similar articles hand-picked by GPT-4
- How to use React Context effectively
- useReducer + useContext for easy global state without libraries
- Why react-hook-form is my new favorite form library
- React context without context, using XState – CodeWithSwiz 14, 15
- How to structure your MobX app for the real world
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️