Someone Special
Someone Special

Reputation: 13588

React - Splitting children into parts

The Idea

I'm trying to create a component that is being split up in to parts.

E.g. in the example code below, I aimed to have the Form and the Actions separated while maintaining state. This way, I can create a basic Form + action button component, and then style it differently on different pages.

import  { useState } from 'react'

const FormBasic = ({ children }) => {
  const [ text, setText ] = useState('')

  const onSubmit = () => console.log("Submitting");
  const onChange = (e) => {
    console.log("Typing", e.target.value);
    setText(e.target.value)
  }

  const Form = () => <textarea rows={4} onChange={onChange} value={text} />;

  const Actions = () => <button onClick={onSubmit}>Some Buttons</button>;

  return children({ Form, Actions });
};

export default FormBasic;

The Problem

I will then be using it this way (below). The thing is - it all renders just fine for static components, but you will start seeing problems the moment state changes.

E.g. When I type in the <textarea /> and onChanged is triggered, the function rerenders and you loses focus to the text area. So end up you cannot really type in the text area.

export default function App() {
  return (
      <FormBasic> 
      {({ Form, Actions }) => {
          return (<React.Fragment>
                  <div style={{ border: '1px solid red'}}>
                    <Form />
                  </div>
                  <div style={{ border: '1px solid blue'}}>
                     <Actions />
                  </div>
            </React.Fragment>)

      }}

      </FormBasic>
  );

}

I'm including a code sandbox - https://codesandbox.io/s/solitary-brook-nrrbf?file=/src/App.js

The Question

There may be questions about the practical use of this kind of component. Practically aside, how may I modify my codes so that I can continue to use the state inside the exported textarea.

Or are there other forms of exporting a component so that I can split them and maintaining the state?

Or is the above style possible at all?

PS: I've used context and redux, and I am trying not to use them in this example. I am trying to keep it simple.

Upvotes: 5

Views: 1378

Answers (1)

Andrei
Andrei

Reputation: 1873

One solution to your problem is: instead of passing text and setText as props, to use the React Context API.

Specifically:

  • FormBasic would become the state + context provider.
  • Form and Actions would become the context consumers.
  • The context would contain the state + the state setter.

Here's what I came up with (working example on CodeSandbox):

(I used more generic names for the state variables to help other readers of this post).

import React from "react";
import { useState, useContext, createContext } from "react";

// create the context with some generic defaults
const StateContext = createContext({
  state: "",
  setState: v => v
});

// create the context hook
const useStateContext = () => useContext(StateContext);

// the textarea component
const Form = () => {
  const { state, setState } = useStateContext();

  const onChange = (e) => {
    console.log("Typing", e.target.value);
    setState(e.target.value);
  };

  return <textarea rows={4} onChange={onChange} value={state} />;
};

// the buttons component
const Actions = () => {
  const { state } = useStateContext();

  const onSubmit = () => {
    alert(state);
  };

  return <button onClick={onSubmit}>Some Buttons</button>;
};

// the form itself holding state and the context provider
const FormBasic = ({ children }) => {
  const [state, setState] = useState("");

  return (
    <StateContext.Provider value={{ state, setState }}>
      {children}
    </StateContext.Provider>
  );
};

const App = () => {
  return (
    <FormBasic>
      <div style={{ border: "1px solid red" }}>
        <Form />
      </div>
      <div style={{ border: "1px solid blue" }}>
        <Actions />
      </div>
    </FormBasic>
  );
};

Notice that FormBasic no longer needs to have children as a function, or the content wrapped in React.Fragment, making the code cleaner. You can use the context consumers directly.

Upvotes: 3

Related Questions