DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for React - Closure that dependency!
Bruno Noriller
Bruno Noriller

Posted on • Originally published at Medium

React - Closure that dependency!

What’s the difference between a custom hook and a functional component?


The (common) problem

You have a component and need to control its state, and it works great:

function BaseExample() {
  const [option, setOption] = useState('two');

  const handleChange = (el) => {
    setOption(el.target.value);
  };

  return (
    <div>
      <select
        onChange={handleChange}
        value={option}
      >
        {[
          { value: 'one', label: 'One' },
          { value: 'two', label: 'Two' },
          { value: 'three', label: 'Three' },
        ].map((option) => (
          <option
            key={option.value}
            value={option.value}
          >
            {option.label}
          </option>
        ))}
      </select>
      <div>{option ? `Selected: ${option}` : 'No selection'}</div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

But what happens when you try to refactor it?

function RefactoredExample() {
  const [option, setOption] = useState('two');

  const handleChange = (el) => {
    setOption(el.target.value);
  };

  return (
    <div>
      {SelectComponent(handleChange, option)}
      <div>{option ? `Selected: ${option}` : 'No selection'}</div>
    </div>
  );
}

function SelectComponent(handleChange, option) {
  return (
    <select
      onChange={handleChange}
      value={option}
    >
      {[
        { value: 'one', label: 'One' },
        { value: 'two', label: 'Two' },
        { value: 'three', label: 'Three' },
      ].map((option) => (
        <option
          key={option.value}
          value={option.value}
        >
          {option.label}
        </option>
      ))}
    </select>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, we have one component that has to know too much and another one that can’t do anything on its own.

Enter custom hooks

By convention normal functional components return JSX and custom hooks can return anything.

Anything? Yes, even JSX.

function RefactoredWithHookExample() {
  const { option, SelectComponent } = useSelectComponent();

  return (
    <div>
      <SelectComponent />
      <div>{option ? `Selected: ${option}` : 'No selection'}</div>
    </div>
  );
}

function useSelectComponent() {
  const [option, setOption] = useState('two');

  const handleChange = (el) => {
    setOption(el.target.value);
  };

  const SelectComponent = () => (
    <select
      onChange={handleChange}
      value={option}
    >
      {[
        { value: 'one', label: 'One' },
        { value: 'two', label: 'Two' },
        { value: 'three', label: 'Three' },
      ].map((option) => (
        <option
          key={option.value}
          value={option.value}
        >
          {option.label}
        </option>
      ))}
    </select>
  );

  return { option, SelectComponent };
}
Enter fullscreen mode Exit fullscreen mode

Now the SelectComponent knows all it needs to control its state and the parent component knows only what it needs.

Anything goes with closures!

An example like this is hardly exciting, but remember that you can return anything from a hook!

Not only that, this can work as a closure, so you could have something like this:

function RefactoredWithClosureHookExample() {
  const { option, SelectComponent } = useSelectComponent({
    options: [
      { value: 'one', label: 'One' },
      { value: 'two', label: 'Two' },
      { value: 'three', label: 'Three' },
    ],
    initial: 'two',
  });

  return (
    <div>
      <SelectComponent
        selectProps={{ style: { color: 'red' } }}
        optionProps={{ style: { color: 'green' } }}
      />
      <div>{option ? `Selected: ${option}` : 'No selection'}</div>
    </div>
  );
}

function useSelectComponent({ options, initial }) {
  const [option, setOption] = useState(initial);

  const handleChange = (el) => {
    setOption(el.target.value);
  };

  const SelectComponent = ({ selectProps, optionProps }) => (
    <select
      onChange={handleChange}
      value={option}
      {...selectProps}
    >
      {options.map((option) => (
        <option
          key={option.value}
          value={option.value}
          {...optionProps}
        >
          {option.label}
        </option>
      ))}
    </select>
  );

  return { option, SelectComponent };
}
Enter fullscreen mode Exit fullscreen mode

This was, of course, an exaggeration. But by understanding what’s possible you’ll be sure to find easier solutions to your problems.


Cover Photo by Jamie MatociΓ±os on Unsplash

Top comments (2)

Collapse
 
lukeshiru profile image
Luke Shiru

This is basically the same as providing stateful components, but with the difference that you're violating the one-way flow by setting parent state from the child. My recommendation instead of returning components from hooks is to make custom hooks that set some properties on stateless components. That way you can reuse the state provided by the custom hooks in other components that share the same properties, you can omit some properties if you want, or listen to changes and so something in the middle, and so on.

A better solution to your problem:

import React, { useCallback, useMemo, useState } from "react";

// You design `SelectComponent` as a stateless component
const SelectComponent = ({ options, optionProps, ...props }) => (
    <select {...props}>
        {options.map(({ value, label }) => (
            <option key={value} {...{ value, ...optionProps }}>
                {optionProps?.children ?? label}
            </option>
        ))}
    </select>
);

// You create a custom hook that returns some `SelectComponent` props
// (could be reused in other components with the same props).
const useInputState = initial => {
    const [value, setValue] = useState(initial);
    const onChange = useCallback(
        ({ currentTarget: { value } }) => setValue(value),
        [],
    );

    return { value, onChange };
};

// Finally you just use the `SelectComponent` combined with the hook...
const RefactoredWithClosureHookExample = () => {
    const selectProps = useInputState("two");

    return (
        <div>
            <SelectComponent
                options={[
                    { value: "one", label: "One" },
                    { value: "two", label: "Two" },
                    { value: "three", label: "Three" },
                ]}
                style={{ color: "red" }}
                optionProps={{ style: { color: "green" } }}
                {...selectProps}
            />
            <div>
                {selectProps.value
                    ? `Selected: ${selectProps.value}`
                    : "No selection"}
            </div>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

I wrote an article about this pattern if you want to take a look.

Cheers!

Collapse
 
noriller profile image
Bruno Noriller Author

The hook with logic is something I already saw to separate logic from JSX.
It makes the code cleaner and you have different things in different places and is certainly something that should be used more.

That said, I had times I stopped at the second refactor because the component was a "one of a kind", it was also inside something that didn't need to know about how the "select" worked internally. So this let me expose to the parent only what it needed to know while hiding the details it didn't need to know.

I wouldn't recommend it save a few occasions you find yourself having to have a lot of details from the child being controlled by the parent. (Also, it's a lot easier to do than using something like an imperative handle.)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.