TypeOfNaN

Schema-Driven React for More Efficient Development

Nick Scialli
August 30, 2020

form schema

I find React to be quite enjoyable front-end tech. However, sometimes I feel like I’m repeating the same old boilerplate in a component with many similar/repeated elements. In this post, I’ll show you how I like to streamline React development at times using schemas that define the components I’m using.

Note that I’ll be using React with Typescript in this post, but this all works just as well without the type defintions!

An Example: A Long Form of Textareas

Let’s say we’re developing some kind of survey application and we want to ask many long-form questions. We know that we’ll need a lot of textareas to ask these questions, and we want to store the answers in an answers object.

Our survey will ask about a bunch of different food preference questions. Our example questions will be:

  • Tell us about your ideal hamburger
  • Tell us about your ideal pizza
  • What is your favorite food memory and why?

Of course, we could go ahead and write out each of these questions as their own textareas. Lets see how that might look.

import React, { useState } from 'react';

const initialAnswers = {
  hamburger: '',
  pizza: '',
  memory: '',
};

function App() {
  const [answers, setAnswers] = useState(initialAnswers);

  return (
    <form>
      <div>
        <label htmlFor="hamburger">Tell us about your ideal hamburger</label>
        <br />
        <textarea
          id="hamburger"
          value={answers.hamburger}
          onChange={(e) => {
            setAnswers({
              ...answers,
              hamburger: e.target.value,
            });
          }}
        />
      </div>
      <div>
        <label htmlFor="pizza">Tell us about your ideal pizza</label>
        <br />
        <textarea
          id="pizza"
          value={answers.pizza}
          onChange={(e) => {
            setAnswers({
              ...answers,
              pizza: e.target.value,
            });
          }}
        />
      </div>
      <div>
        <label htmlFor="memory">
          What is your favorite food memory and why?
        </label>
        <br />
        <textarea
          id="memory"
          value={answers.memory}
          onChange={(e) => {
            setAnswers({
              ...answers,
              memory: e.target.value,
            });
          }}
        />
      </div>
    </form>
  );
}

This is perfectly fine, and it works. But what if we have ~20 questions like this? We’re going to get pretty tired replicating these components. This is where a schema approach really shines.

Using a Schema to Simplify Repetitive Component Development

Instead of the apporach above, let’s make some optimizations that will make development less tedious. First, we’re going to make a single change handler that can handle changes for any of the fields in our form. Next, we’re going to create an array of objects that each represent a field in our form and we will use that to generate all our questions.

First, let’s create that consistent onChange handler.

import React, { useState } from 'react';

const initialAnswers = {
  hamburger: '',
  pizza: '',
  memory: '',
};

type Answers = typeof initialAnswers;

function App() {
  const [answers, setAnswers] = useState(initialAnswers);

  const onChange = <K extends keyof Answers>(key: K, value: Answers[K]) => {
    setAnswers({
      ...answers,
      [key]: value,
    });
  };

  return <></>;
}

Next, we can add in an array of fields to iterate over.

import React, { useState } from 'react';

const initialAnswers = {
  hamburger: '',
  pizza: '',
  memory: '',
};

type Answers = typeof initialAnswers;

type Field = {
  key: keyof Answers;
  label: string;
};

const fields: Field[] = [
  { key: 'hamburger', label: 'Tell us about your ideal hamburger' },
  { key: 'pizza', label: 'Tell us about your ideal pizza' },
  { key: 'memory', label: 'What is your favorite food memory and why?' },
];

function App() {
  const [answers, setAnswers] = useState(initialAnswers);

  const onChange = <K extends keyof Answers>(key: K, value: Answers[K]) => {
    setAnswers({
      ...answers,
      [key]: value,
    });
  };

  return <></>;
}

Finally, we can iterate over the array rather than typing out every textarea component.

import React, { useState } from 'react';

const initialAnswers = {
  hamburger: '',
  pizza: '',
  memory: '',
};

type Answers = typeof initialAnswers;

type Field = {
  key: keyof Answers;
  label: string;
};

const fields: Field[] = [
  { key: 'hamburger', label: 'Tell us about your ideal hamburger' },
  { key: 'pizza', label: 'Tell us about your ideal pizza' },
  { key: 'memory', label: 'What is your favorite food memory and why?' },
];

function App() {
  const [answers, setAnswers] = useState(initialAnswers);

  const onChange = <K extends keyof Answers>(key: K, value: Answers[K]) => {
    setAnswers({
      ...answers,
      [key]: value,
    });
  };

  return (
    <form>
      {fields.map(({ key, label }) => (
        <div key={key}>
          <label htmlFor={key}>{label}</label>
          <br />
          <textarea
            id={key}
            value={answers[key]}
            onChange={(e) => {
              onChange(key, e.target.value);
            }}
          />
        </div>
      ))}
    </form>
  );
}

export default App;

And there we have it! Now we can add as many fields to our array as we need and new textareas will be automagically added to the app.

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

Nick Scialli

Nick Scialli is a senior UI engineer at Microsoft.

© 2024 Nick Scialli