Thinking in React: The 2020 version

Is 2020 and the original Thinking in React article still has class components in it, so I feel is time to do an updated version of it:

Start mocking

We should always start with a mock, either provided by a designer/design team in those big projects or made by ourselves if it's a small personal project. So let's say we want the classic login experience:

Login Mockup

Break mock into components

Now that we have the mock, we need to take a look at it and identify its parts:

Login Mockup with highlighted parts

Once identified, we should use clear names for every "component" (PascalCase by React convention):

  • LoginForm (red): The whole login form.
  • SubmitButton (green): The button to submit the "form".
  • Label (pink): The form labels.
  • Input (orange): The form inputs.
  • PasswordInput (light blue): The form input with type password.

Build components

Now that we have identified the components, let's build them!

const Label = props => <label {...props} />;

const Input = props => <input {...props} />;

const PasswordInput = props => <Input type="password" {...props} />;

const SubmitButton = props => <button type="submit" {...props} />;

const LoginForm = props => <form {...props} />;

Notice, that we can even reuse Input inside PasswordInput.

Use components

Now that we have those components, we can use them to bring our mock to life. Let's call this wrapping component LoginContainer:

const LoginContainer = () => (
	<LoginForm>
		<Label>
			Username
			<Input name="username" />
		</Label>
		<Label>
			Password
			<PasswordInput name="password" />
		</Label>
		<SubmitButton>Login</SubmitButton>
	</LoginForm>
);

This needs API interaction and event handling, but first...

Early optimizations

While working on the components, we might detect optimizations such as every time we use an Input or PasswordInput component, we wrap it in a Label, so in order to keep DRY, let's make a curried function to wrap in Label:

const labeled =
	Input =>
	({ children, labelProps, name, ...props }) =>
		(
			<Label {...labelProps}>
				{children}
				<Input {...{ name, ...props }} />
			</Label>
		);

And now we can create two new components with that:

const FormInput = labeled(Input);
const FormPasswordInput = labeled(PasswordInput);

So now, our LoginContainer looks like this:

const LoginContainer = () => (
	<LoginForm>
		<FormInput name="username">Username</FormInput>
		<FormPasswordInput name="password">Password</FormPasswordInput>
		<SubmitButton>Login</SubmitButton>
	</LoginForm>
);

Adding state

The state should generally be left for last, thinking and designing everything as stateless as possible, using props and events. It makes components easier to maintain, test, and overall understand.

If you need a state, it should be handled by either state containers (Redux, MobX, unistore, and so on) or a container/wrapper component. In our super-simple login example, the place for the state could be LoginContainer itself, let's use React hooks for this:

const LoginContainer = () => {
	const [username, setUsername] = useState("");
	const [password, setPassword] = useState("");
	const login = useCallback(
		event => {
			event.preventDefault();

			fetch("/api", {
				method: "POST",
				body: JSON.stringify({ username, password }),
			})
				.then(response => response.json())
				.then(response => {
					// Handle the response somehow
				})
				.catch(console.error);
		},
		[username, password],
	);

	return (
		<LoginForm onSubmit={login}>
			<FormInput
				name="username"
				onChange={({ currentTarget }) =>
					setUsername(currentTarget.value)
				}
				value={username}
			>
				Username
			</FormInput>
			<FormPasswordInput
				name="password"
				onChange={({ currentTarget }) =>
					setPassword(currentTarget.value)
				}
				value={password}
			>
				Password
			</FormPasswordInput>
			<SubmitButton>Login</SubmitButton>
		</LoginForm>
	);
};

The approach of avoiding state is related to Functional Programming principles but basically is to keep the components as pure as possible.

TL;DR

  1. Mock.
  2. Identify components.
  3. Build them.
  4. Use them (and optimize them when needed).
  5. Try to stay as stateless as possible. Add state only if needed.

That's it! Thank you for reading!

Leave a comment