Inner Hooks: New idea of React Hooks

tkow

tkow

Posted on February 11, 2022

Inner Hooks: New idea of React Hooks

Introduction

This post is about my idea recently came to my mind though it may be not completely original if I research all the way around.

It's not official concept from react dev teams or facebook. I'm just a programmer from everywhere though a little experienced to some extent. So, my idea may not satisfy you but, I want to explain and discuss new concept about react hooks with everyone who is interested in it. I call it "Inner Hooks".

I experimentally make library following this concept. Here is my repository of it. And try it in playground if you want.

What is Inner Hooks

Inner Hooks idea makes just react-hooks available in a component's child scope by props passing. Nothing more, nothing less. My library realize by creating HOC.

Why I need this?

It'll be a rehash from my repository README document, but I explain the advantage. If you interested in this, please see also that.

At first, we should have written hooks before starting jsx description. For example, we couldn't write hooks between conditional rendered components like followed by an example because it violates rendering hooks rules about idempotent.

const Example = (props) => {
  const { initialized, data } = useFetchData();
  if (!initialized) return null;
  const options = useOptions();
  return <Component {...data} options={options} />;
};
Enter fullscreen mode Exit fullscreen mode

This fact may annoy you if you come across excessive fat component. I show example you may feel so.

const Example = (props) => {
    const options = useOptions()
    const [optionValue, setOptionValue] = useState()
    const {initialized, data} = useFetchData()
    const someValue = ''
    const someChange = () => {}
    if (!initialized) return null
    return (
        <Component>
            <Child>
              <AnnoyedField
                value={someValue}
                onChange={someChange}
                class='test'
                otherProps
              />
              <AnnoyedField
                value={someValue}
                onChange={someChange}
                class='test'
                otherProps
              />
              <AnnoyedField
                value={someValue}
                onChange={someChange}
                class='test'
                otherProps
              />
              <AnnoyedField
                value={someValue}
                onChange={someChange}
                class='test'
                otherProps
              />
              <AnnoyedField
                value={someValue}
                onChange={someChange}
                class='test'
                otherProps
              />
              <AnnoyedField
                value={someValue}
                onChange={someChange}
                class='test'
                otherProps
              />
              <AnnoyedField
                value={someValue}
                onChange={someChange}
                class='test'
                otherProps
              />
              <Select
                value={optionValue}
                onChange={setOptionValue}
                options={options}
                otherProps
              />
              <AnnoyedField
                value={someValue}
                onChange={someChange}
                class='test'
                otherProps
              />
              <AnnoyedField
                value={someValue}
                onChange={someChange}
                class='test'
                otherProps
              />
            <Child/>
        </Component>
    )
}
Enter fullscreen mode Exit fullscreen mode

It's written in declarative way, you still can read if you don't want. In real, the handler is possible arrow function and some amateur engineers may write long code directly without abstraction. If do so, it's tough to find scope of changing state effects or where state are derived from used in event handlers.

We once solved the problem using the container component to
inject concrete behaviors per loosely-coupled component like IOC(Inversion of Control) theory, but there is defect that to do this you need separate some children components from the parent. The alternative is react hooks can be mixed encapsulated logics and components in one component. But hooks can also have a weak point as having ever seen examples above.

Eventually, you'll find it might be better to separate hooks and presentational component like container layer as bigger it is though it can put them together to one component.

InnerHooks tackles this problem and realize it can completely encapsulate the business logic to one component in some cases.

For example, if you use Redux,

    <NumberInput
      innerHooks={() => {
        const num = useSelector(({num}) => { return num})
        const dispatch = useDispatch()
        return {
          value,
          onChange: (e) => {
            dispatch({type: 'mutateNum', payload: num})
          }
        }
      }}
    />
Enter fullscreen mode Exit fullscreen mode

I realize that withInnerHooks api generate hoc add innerHooks prop are called in intermediate layer of hoc to inputed Component. The innerHooked returned value are merged with the other props specified from the component tag.

you write once this, you can use or move it another place everywhere with cut and paste. This is more convenient in some case than you obey strictly React's hooks rendering rules and declarative policy.

From my playground example, you can find they are loosely-coupled and separate independent logic

import "./styles.css";
import React, { useEffect } from "react";
import { useStateFactory, withInnerHooks } from "react-inner-hooks-extension";

function Child(props: any) {
  return (
    <div>
      <input type={props.type} onChange={props.onChange} value={props.value} />
    </div>
  );
}

function Text(props: any) {
  return <div>{props.value}</div>;
}

const NumberField = withInnerHooks(Child);
const StringField = withInnerHooks(Child);
const Timer = withInnerHooks(Text);

export default function App() {
  const [state, usePartialState, setState] = useStateFactory<{
    num: number;
    str: string;
    timer: number;
  }>({
    num: 1,
    str: "foo",
    timer: 0
  });

  return (
    <div className="App">
      <form
        onSubmit={(e) => {
          e.preventDefault();
          // dummy submit method
          const submit = console.log;
          submit(state);
          // RestState
          setState({
            num: 1,
            str: "foo",
            timer: 0
          });
        }}
      >
        <NumberField
          type="number"
          innerHooks={() => {
            const [value, setValue] = usePartialState("num");
            return {
              value,
              onChange: (e: any) => setValue(e.target.value)
            };
          }}
        />
        <StringField
          type="string"
          innerHooks={() => {
            const [value, setValue] = usePartialState("str");
            return {
              value,
              onChange: (e: any) => setValue(e.target.value)
            };
          }}
        />
        <input type="submit" value={"reset"} />
      </form>
      <Timer
        innerHooks={() => {
          const [value, setValue] = usePartialState("timer");
          // This is warned by linter but it can be used.
          useEffect(() => {
            const i = setInterval(() => {
              setValue((state: number) => state + 1);
            }, 1000);
            return () => {
              clearInterval(i);
            };
          }, []);
          return {
            value
          };
        }}
      />
      <div>current:{JSON.stringify(state)}</div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, each components enclose only the related logic in prop scope.

These can be written like as the container in declarative way . The difference is that you can determine how it behaves in
parent component scope.

const useHooksContainer = () => {
  const num = useSelector(({num}) => { return num})
  const dispatch = useDispatch()
  return {
    value,
    onChange: (e) => {
      dispatch({type: 'mutateNum', payload: num})
    }
  }
}

() => (
    <NumberInput
      innerHooks={useHooksContainer}
    />
)
Enter fullscreen mode Exit fullscreen mode

Concern

Inner hooks look opposed to React declarative policy but though it can also be encapsulated and abstracted by custom hooks. And I think this feature should be equipped in React library itself or extend its render function as possible for more effective about performance and avoidance to repeat to write withInnerHooks hoc any where. If you use eslint with several react-hooks rules, this library violates some of them. So you may need to ignore them.

Wanted Your Opinions!

Please feel free to post your opinions in discussion. Thanks to read.

In Addition at 2022/02/17

Referencing this discussion, I could improve my library for our sake. Thank you for everyone who joins it!

💖 💪 🙅 🚩
tkow
tkow

Posted on February 11, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related