DEV Community

tkow
tkow

Posted on • Updated on

 

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!

Top comments (19)

Collapse
 
lukeshiru profile image
Luke Shiru

This goes against the rules of hooks (you're putting hooks inside callbacks). If it's tedious for you to write something like this:

import { useState } from "react";
import type { VFC } from "react";
import "./styles.css";

export const ExampleForm: VFC<JSX.IntrinsicElements["form"]> = props => {
    const [number, setNumber] = useState(0);
    const [string, setString] = useState("");

    return (
        <form {...props}>
            <input
                type="number"
                value={number}
                onChange={({ currentTarget }) =>
                    setNumber(parseInt(currentTarget.value, 10))
                }
            />
            <input
                value={string}
                onChange={({ currentTarget }) => setString(currentTarget.value)}
            />
            <button type="submit">Submit</button>
        </form>
    );
};
Enter fullscreen mode Exit fullscreen mode

You can always have a small util to handle state for inputs, like this:

import type { ChangeEventHandler } from "react";
import { useState } from "react";

const useInputState = (
    defaultValue?: JSX.IntrinsicElements["input"]["value"],
) => {
    const [value, setValue] = useState(defaultValue);

    const onChange: ChangeEventHandler<HTMLInputElement> = ({
        currentTarget,
    }) => setValue(currentTarget.value);

    return [{ value, onChange }, setValue] as const;
};
Enter fullscreen mode Exit fullscreen mode

And then use it like this:

import type { VFC } from "react";
import { useInputState } from "../utils/useInputState.js";
import "./styles.css";

export const ExampleForm: VFC<JSX.IntrinsicElements["form"]> = props => {
    const [numberProps] = useInputState(0);
    const [stringProps] = useInputState("");

    return (
        <div className="App">
            <form {...props}>
                <input type="number" {...numberProps} />
                <input {...stringProps} />
                <button type="submit">Submit</button>
            </form>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

And if you want to handle it like an object, then maybe you can use useReducer instead of useState ... or you could go and use one of the many libraries for form state that are out there like react-hook-form ... or you could just use something like remix that kinda gets rid of state management for forms altogether.

But yup, this approach is basically against the rules of hooks, so it will break for example if you optionally render parts of your form that have that innerHooks property on them.

Cheers!

Collapse
 
tkow profile image
tkow • Edited

This approach is basically against the rules of hooks

Yes, so I want to discuss you feel the advantage even if it leads to change hooks rules.
Your opinion seem opposite to that and I understand you say we should write code in manner of React.
Thank you many examples. I also think they are React's standard way and ordinary use them for me, too.
I' m glad to know there are many experts who think my approach is not attractive as I expected.

Collapse
 
tkow profile image
tkow • Edited

For you, I'll show you my partial react-admin code in my project which written in declarative way as long as I can about selling book data. TabbedForm component has implicitly context provider in it. I want to ask you want to insert FormDataConsumer or extract components for using context data will be redundant expression and increase files if you use single component style as on a basis you need it. If you don't so, my library should mean it.

(
    <Edit
      {...props}
      transform={transform}
      title={<NameTitle resourceName="Book" />}
    >
      <TabbedForm
        toolbar={<WithoutDeleteButtonEditToolbar />}
        submitOnEnter={false}
      >
        <FormTab label={'resources.Book.tabs.book_info' as TranslateKeys}>
          <TextInput disabled label="Id" source={properties.id} />
          {isAdmin && (
            <ReferenceInput reference="Publisher" source="publisher_id">
              <AutocompleteInput source="id" optionText="name" alwaysOn />
            </ReferenceInput>
          )}
          <TextInput source={properties.name} fullWidth />
          <NumberInput source={properties.price} />
          <SelectInput
            source={properties.price_unit}
            choices={priceUnitOptions}
          />
          <TextInput source={properties.isbn_code} />
          {/* Author Start*/}
          <ArrayInput validate={required()} source="authors">
            <SimpleFormIterator>
              <TextInput
                label="resources.Book.fields.author.name"
                validate={required()}
                source="name"
              />
              <SelectInput
                label="resources.Book.fields.author.type"
                choices={authorTypeOptions}
                validate={required()}
                source="type"
              />
            </SimpleFormIterator>
          </ArrayInput>
          {/* Author End */}
          <TextInput source={properties.label} />
          <NumberInput source={properties.page} />
          <SelectInput
            source={properties.format}
            choices={selectBoxOptions(BookFormat)}
          />
          <DateInput source={properties.published_at} validate={required()} />
          <FileInput
            source={properties.file_path}
            validate={required()}
            title="title"
            accept={['application/epub+zip', 'application/pdf'].join(',')}
            placeholder={<p>Drop your file here</p>}
          >
            <PreviewFileField source="src" title="src" />
          </FileInput>
          <ImageInput
            source={properties.image_url}
            accept="image/*"
            placeholder={<p>Drop your image file here</p>}
            validate={required()}
          >
            <PreviewImageField source="src" />
          </ImageInput>
          <TextInput source={properties.publisher_accounting_code} />
          <FormDataConsumer>
            {({
              formData
            }) => {
              return (
                <TextInput
                  resource={'Book'}
                  defaultValue={formData.id}
                  source={'bookCheck.book_id'}
                  disabled
                />
              );
            }}
          </FormDataConsumer>
          {isAdmin && (
            <SelectInput
              source={'bookCheck.service_type'}
              defaultValue={ServiceType.default}
              choices={selectBoxOptions(ServiceType)}
              disabled
            />
          )}
          <SelectInput
            source={'bookCheck.availability'}
            choices={availabilityOptions}
          />
          {isAdmin && (
            <SelectInput
              source={'bookCheck.status'}
              choices={checkStatusOptions}
            />
          )}
          {isAdmin && <BooleanInput source={properties.is_deleted} />}
          {!isAdmin && (
            <FunctionField
              source="bookCheck.status"
              render={(record: any, source?: string | undefined) => {
                return t(
                  `resources.Book.fields.CheckStatus.${record['bookCheck']['status']}`,
                );
              }}
            />
          )}
        </FormTab>
   </ TabbedForm>
</Edit>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
lukeshiru profile image
Luke Shiru • Edited

The rules of hooks aren't there because React wanted to put rules. The rules of hooks are there because if you don't follow them you produce bugs. If you put a hook inside a callback, you risk not calling it in the same order, and the order the hooks are called is key for them to work. Here, read this.

Thread Thread
 
tkow profile image
tkow • Edited

The rules of hooks aren't there because React wanted to put rules. The rules of hooks are there because if you don't follow them you produce bugs. If you put a hook inside a callback, you risk not calling it in the same order, and the order the hooks are called is key for them to work. Here, read this.

It looks so for you, so I restrict special prop named innerHooks generating hoc to have to call it in the child container component. This is not breaking and not related to parent if you use it in same manner of it because it makes sandbox component separated from both the parent and the child. If you take it as invalid, you answer my questions and I'll appreciate it.

Thread Thread
 
lukeshiru profile image
Luke Shiru

Think it this way, hooks always need to be called in the same order in order to work, using your approach, we could write something like this:

<>
    {someBoolean ? (
        <ExampleComponentA
            innerHooks={() => {
                const [value, setValue] = usePartialState("valueA");
                // do something with value
            }}
        />
    ) : undefined}
    <ExampleComponentB
        innerHooks={() => {
            const [value, setValue] = usePartialState("valueB");
            // do something with value
        }}
    />
</>
Enter fullscreen mode Exit fullscreen mode

And the problem with it is that someBoolean is true, we call the first usePartialState, and the second usePartialState, which is similar to doing this:

const [value, setValue] = usePartialState("valueA");
const [value, setValue] = usePartialState("valueB");
Enter fullscreen mode Exit fullscreen mode

But if someBoolean is false, we only call the second one:

const [value, setValue] = usePartialState("valueB");
Enter fullscreen mode Exit fullscreen mode

This effectively changes the order of the calls. In order for the hooks to work correctly, they need to allways be called in the same order, so that's why we have rules like avoid putting them inside loops, or ifs, or callbacks ... they have to be at the top level of the function.

Thread Thread
 
tkow profile image
tkow • Edited

I see. I agree with the looks confused though our libraries can work in your situations. Thank you!

Collapse
 
alfredosalzillo profile image
Alfredo Salzillo

I think this approach is more prone to error and breaking hooks rules.
Also, i don't see any benefit in this, the version with custom hooks is more easy and simple to understand.

Collapse
 
tkow profile image
tkow • Edited

Intresting.

this approach is more prone to error and breaking hooks rules

I want you to tell examples if you can.
Ofcourse, I know it looks breaking them, but my suggestion hooks called inside intermediate component, in fact, wrapped effects called are not breaking as long as you use innerHooks correctly in hooks rules.

The rules are rules, so should be changeable if don't match our benefits.

Most useful case of my approach is to wrap context one component. You can do this

const Context = createContext({a: 0, b:0})
() => {
  return (
     <Context.Provider>
        <Field 
           innerHooks={
              () => {
                 const {a: value} = useContext(Context)
                 return {value}
              }
           }
        />
       <Field 
           innerHooks={
              () => {
                 const {b: value} = useContext(Context)
                 return {value}
              }
           }
        />
     </Context.Provider>
  )
} 
Enter fullscreen mode Exit fullscreen mode

If you want to do without this. Now,

const Context = createContext({a: 0, b:0})
() => {
  return (
     <Context.Provider>
        <Context.Consumer>
          {({a,b}) => (
              <Field 
                 value={a}
              />
              <Field 
                 value={b}
              />
          )}
       <Context.Consumer>
     </Context.Provider>
  )
} 
Enter fullscreen mode Exit fullscreen mode

This may annoy you because consumer scope can become wide and deep, thus they are possible to become tough to read even if you write them declarative. Consumer is confused about how deep it is or how wide it is . Furthermore, this intend to cause performance problem if you have some wrong to set appropriate scope.When you can't useContext in parent hooks scope because Provider still isn't rendered so, we had only way to Consumer function like an example above ever. There are libraries use Provider component and, you have no choice except to separate components or consumer function above if you don't use innerHooks.

React Admin match this concept. The presentational component is simple from ui-material, but if you want this context, your DX become worse because nonetheless their field components are deeply-coupled you need separate them or have deep nest by consumer.

In addition, React rendering rules should avoid deeper component tree as long as possible, because it's tough to find where the state derived from and as it's closer to parent it can call un-effective render many times. In the perspective of architecture, I think presentational components should become independent, but
don't need state injected components do so. My way can avoid deeper tree if your components are not so complicated.

But, I also learn from your point. I should make it clear that it is useful in some limited situations.
Thank you for opinion!

Collapse
 
alfredosalzillo profile image
Alfredo Salzillo

In this example make no sense to use a context. Context should be use only if data must be shared from components that have a distance > 2 in the hierarchy.
This approach make you use context the wrong way.

Thread Thread
 
tkow profile image
tkow • Edited

Yeah of course I know what you tell.
Normally, context provider should be upper component in tree. This is for explanation.
There are some unique libraries implicitly provide limited scoped context like react-final-form its provider provide all form values via the context and hooks to access the context.
The hooks can't be used before start rendering because the scope is outer provider. So you must use consumer like above if you want to implement all logic to one component, or you'll split it to somewhat many field components with their state hooks. Form is deeply-coupled so split components per state is not so useful. Thus there are some cases to rewrite consumer to innerHooks like above means. In the other words, this is for the users of the libraries like react-admin form or react-final-form. If you use them, you'll understand what I mean. If not so, sorry I couldn't help without saying this may be not for you.

And innerHooks can write in declarative way as I added latest explanation in this article.

Collapse
 
ivan_jrmc profile image
Ivan Jeremic

This is totally valid React.

Collapse
 
alfredosalzillo profile image
Alfredo Salzillo

I'm not saying is invalid React, I'm only saying it's more complex than using React the simple way, and it's not needed.
In the examples made, the code look worst than abstracting using custom hooks or other solution.
It's more difficult to read and understand.
Also innerHooks mean nothing, it's not clear how you have to use it, what should the function return? If someone read this code after one month will not understand why it's done this way and what it's doing whiteout enter the Field component.

Good code must be readable and understandable. This is not.

Thread Thread
 
ivan_jrmc profile image
Ivan Jeremic • Edited

Yeah agree the example above doesn't make any sense however I would like to see a dom node hook api like I said this makes sense indeed, if you are not familiar with it try the element hooks which Svelte.js has quickly in Codesandbox , they are awesome.

Thread Thread
 
tkow profile image
tkow • Edited

My library written in typescript and type inference works as you won't lose what totally you should return as partial props and rest props from component is validated by inferred rest props and vise versa. The returned value allows only key-value of the partial component props.

I'm on your side about with no abstraction code is very bad for general components (Most of them are presentational), but there are some limited cases you may not come across that build and scrap each condensable components in one component is preferred to courteous declarative or split components. These will need just only container injection in most cases.

Please be tolerant. I don't think your opinion is wrong. Please don't fight anyone for the reason they are different from you to make meaning discussion though it may be needless to say.

Collapse
 
ivan_jrmc profile image
Ivan Jeremic

I used that silently already for months thinking I'm the only one. :D And by the way I hope React gets element hooks like Svelte instead of using useRef to target element, how cool would it be to have a custom element hook and then use it

Collapse
 
tkow profile image
tkow

Wow, your idea about getting element hooks looks really nice for me.
If you've found how it's realized, I really want you to widely make it public.
I'm glad to hear you're doing for a while. I also feel alone and anxiety because I don't know my idea is bad or not lol.
It should be nice enough if you're doing this. Thank you.

Collapse
 
tkow profile image
tkow

I recentely add useSharedRef API to my library which not make perfectly element callable everywhere but, may be near concept. Ref is now

callable everywhere your hooks scope if it set arbitrary component.

Collapse
 
tkow profile image
tkow • Edited

Maybe the name innerHooks leads to miss my concept. It may be connectContainer suitable it can be used without hooks and I renamed them in my library.

Visualizing Promises and Async/Await ๐Ÿค“

async await

โ˜๏ธ Check out this all-time classic DEV post on visualizing Promises and Async/Await ๐Ÿค“