Planning a React component

This article assumes that all react components in a code base are Functional React Components as opposed to Class Components.

Whenever we write a React feature, we should break the feature into a set of responsibilities. These responsibilities can be categorized into different types of React components. In other words, we will discuss a structural pattern for a react codebase if we imagine it to be composed of several features. We have seen articles discussing structural patterns for react codebase based on Gang of Four (GoF) design patterns but we have been careful in borrowing ideas from (GoF) because GoF is meant for Object-Oriented Programming (OOP) and the nature of the react code base under discussion is closer to Functional Programming than OOP.

In this article, we are trying our best to give a single responsibility to each component type.

Type of React Components

  • Data Manager Component - The data for the feature might come via the network or the app's state manager etc. Also, the user events like a text from an input box are sent to the backend via network or stored in the app's state manager. This component is usually the entry point to the feature. This component can have Layout Components and Presentational Components as its children. As a consequence of having this data manager component, we will have to pass props down to child components, and bubble up callbacks from child components to this data manager component. We have to bubble up callbacks so that the data manager component gets access to the user events and sends it further over the network or to the app's state manager. As we ar promoting this pattern of having a data manager as a root component for the feature, we encourage you to follow Composition over Inheritance to reduce because it will prevent you from passing props and callbacks too deep.

  • Layout Component - There are two types of layout components based on how child components can be passed to the layout component.

    • Higher-Order Component - Functions (functional components) are passed on as children. In the JSX of a higher-order component, you might write <ChildComponent /> somewhere.

    • Container Component - React Nodes are passed on as children. In the JSX of a container component, you might write {props.children} somewhere.

Some other resources on the web allow data fetching in the container components, but we highly discourage it. The reason is that when the container component re-renders, the{props.children} does not re-render. Please note that when you write <Component />, this gets converted into React.createElement(Component, {}) and React.createElement function returns a ReactNode. In contrast, {props.children} is already a ReactNode hence, its not wrapped inside React.createElement function when the JSX is transpiled into JS. In other words, {props.children} is actually the return value of React.createElement(Component, {})/<Component />. Since, {props.children} is already a ReactNode it cannot be re-rendered whenever it's parent component is rendered.

  • Presentational Component - Two types

    • Smart Component - Has internal state (A developer needs to be smarter to write a smart component).

    • Dumb Component - Does not have an internal state.

A presentational component can be a controlled component. Please do not mix the layout component with the controlled component as we try to keep the layout component as much as possible free of any internal state. The only internal state a layout component should have are the ones that control the layout (such as hiding or showing a component depending upon some user action that may be part of the layout component).

When we follow the above structural pattern for distributing responsibilities, it becomes imperative to follow props-down, actions-up because we generally have a single data manager component for a feature i.e. all the data-related side effects are located in one root component for this feature.

In a controlled component, don't manage the state of the form element internally, but rather pass it directly via callback up the component tree and similarly, get the value for the controlled component directly from props.

The incorrect way to write controlled component

function CustomInput(props: { cb: (text: string) => void }) {
    // internal state in a controlled component
    const [text, setText] = useState("");
    useEffect(()=>{
        props.cb(text)
    }, [text])

    return (
        <input
            onChange={(change) => {
                setText(change.value);
            }}
            value={text}
        />
    );
}

The correct way to write controlled component

function CustomInputRightWay(props: { cb: (text: string) => void, text: string }) {
    // no internal state in the controlled component
    return (
        <input
            onChange={(change) => {
                props.cb(change.value)
            }}
            value={props.text}
        />
    );
}

// Assuming the Data Manager component looks something like this
function DataManangerComponent(cb){
    const [text, setText] = useState("")
    const cb = useCallback((text)=>{
      setText(text)
    }, [])
    useEffect(()=>{
      // Maybe sent the text to backend
      // Or store in the app's state manager
    },
    [text])
    return <CustomInput cb={cb} text={text}/>
}

Key Takeaways

  • A complete structural pattern for React.

  • Difference between {props.children} and <Component/>.

  • Writing a controlled component without an internal state.