DhiWise
Published in

DhiWise

React Component Patterns

Overview

Compound Components

Overview

Why use compound components? What value do they provide?

Example

{/* The parent component that handles the onChange events 
and managing the state of the currently selected value. */}
<RadioImageForm>
{/* The child, sub-components.
Each sub-component is an radio input displayed as an image
where the user is able to click an image to select a value. */}
<RadioImageForm.RadioInput />
<RadioImageForm.RadioInput />
<RadioImageForm.RadioInput />
</RadioImageForm>
  1. RadioImageForm - First, we create the parent component that will manage the state and handle the on-change events of the form. The consumer of the component, other engineers using the component, can subscribe to the currently selected value of the radio inputs by passing a callback function prop, onStateChange. With each form change, the component will handle updating the radio inputs and provide the current value to the consumer.
  1. RadioInput - Next, we will create a static component, a subset component of the RadioImageForm component. The RadioInput is a static component that is accessible through the dot-syntax notation, e.g. <RadioImageForm.RadioInput/>. This allows the consumer of our components to readily access our sub-components and provide them with control of how they RadioInput are rendered within the form.
export class RadioImageForm extends React.Component<Props, State> {  static RadioInput = ({
currentValue,
onChange,
label,
value,
name,
imgSrc,
key,
}: RadioInputProps): React.ReactElement => (
//...
);
onChange = (): void => {
// ...
};
state = {
currentValue: '',
onChange: this.onChange,
defaultValue: this.props.defaultValue || '',
};
render(): React.ReactElement {
return (
<RadioImageFormWrapper>
<form>
{/* .... */}
</form>
</RadioImageFormWrapper>
)
}
}
render(): React.ReactElement {
const { currentValue, onChange, defaultValue } = this.state;
return (
<RadioImageFormWrapper>
<form>
{
React.Children.map(this.props.children,
(child: React.ReactElement) =>
React.cloneElement(child, {
currentValue,
onChange,
defaultValue,
}),
)
}
</form>
</RadioImageFormWrapper>
)
}
  1. RadioImageFormWrapper - Our component styles with styled-components. We can ignore this as the CSS styles do not pertain to the component pattern.
  2. React.Children.map - It iterates through the component's direct children, allowing us to manipulate each direct child.
  3. React.cloneElement - From the React docs:
static RadioInput = ({
currentValue,
onChange,
label,
value,
name,
imgSrc,
key,
}: RadioInputProps) => (
<label className="radio-button-group" key={key}>
<input
type="radio"
name={name}
value={value}
aria-label={label}
onChange={onChange}
checked={currentValue === value}
aria-checked={currentValue === value}
/>
<img alt="" src={imgSrc} />
<div className="overlay">
{/* .... */}
</div>
</label>
);
// src/index.tsx
<RadioImageForm onStateChange={onChange}>
{DATA.map(
({ label, value, imgSrc }): React.ReactElement => (
<RadioImageForm.RadioInput
label={label}
value={value}
name={label}
imgSrc={imgSrc}
key={imgSrc}
/>
),
)}
</RadioImageForm>

Conclusion

Drawbacks

⬆️ Compound Components CodeSandBox

⬆️ Compound Components w/ functional components CodeSandBox

⬆️ Flexible Compound Components

Overview

Why use flexible compound components? What value do they provide?

Example

const RadioImageFormContext = React.createContext({
currentValue: '',
defaultValue: undefined,
onChange: () => { },
});
RadioImageFormContext.displayName = 'RadioImageForm';
  1. By calling React.createContext we have created a context object containing a Provider and Consumer pair. The former will provide data to the latter; in our example, the Provider will expose our internal state to the sub-components.
  2. By assigning a displayName to our context object, we can easily differentiate between context components in React Dev Tool. So instead of having Context.Provider or Context.Consumer we will have RadioImageForm.Provider and RadioImageForm.Consumer. This helps readability if we have multiple components using Context while debugging.
render(): React.ReactElement {
const { children } = this.props;
return (
<RadioImageFormWrapper>
<RadioImageFormContext.Provider value={this.state}>
{children}
</RadioImageFormContext.Provider>
</RadioImageFormWrapper>
);
}
export class RadioImageForm extends React.Component<Props, State> {
static Consumer = RadioImageFormContext.Consumer;
//...
static SubmitButton = ({ onSubmit }: SubmitButtonProps) => (
<RadioImageForm.Consumer>
{({ currentValue }) => (
<button
type="button"
className="btn btn-primary"
onClick={() => onSubmit(currentValue)}
disabled={!currentValue}
aria-disabled={!currentValue}
>
Submit
</button>
)}
</RadioImageForm.Consumer>
);

Conclusion

⬆️ Flexible Compound Component CodeSandBox

⬆️ Flexible Compound Components w/ Functional Components CodeSandBox

⬆️ Provider Pattern

Overview

Why use provider patterns? What value do they provide?

Example

// src/components/DogDataProvider.tsx
interface State {
data: IDog;
status: Status;
error: Error;
}
const initState: State = { status: Status.loading, data: null, error: null };const DogDataProviderContext = React.createContext(undefined);
DogDataProviderContext.displayName = 'DogDataProvider';
const DogDataProvider: React.FC = ({ children }): React.ReactElement => {
const [state, setState] = React.useState<State>(initState);
React.useEffect(() => {
setState(initState);
(async (): Promise<void> => {
try {
// MOCK API CALL
const asyncMockApiFn = async (): Promise<IDog> =>
await new Promise(resolve => setTimeout(() => resolve(DATA), 1000));
const data = await asyncMockApiFn();
setState({
data,
status: Status.loaded,
error: null
});
} catch (error) {
setState({
error,
status: Status.error,
data: null
});
}
})();
}, []);
return (
<DogDataProviderContext.Provider value={state}>
{children}
</DogDataProviderContext.Provider>
);
};
  1. First off, we create a context object, DogDataProviderContextwith React's Context API via React.createContext. This will be used to provide state to consuming components with a custom React hook that we will implement later.
  2. By assigning a displayName to our context object, we can easily differentiate between context components in React Dev Tool. So instead of having Context.Provider we will have DogDataProvider.Provider in our React Dev Tools. This helps readability if we have multiple components using Context while debugging.
  3. In our useEffect the hook we will fetch and manage the same shared data that will be consumed by multiple child components.
  4. The model of our state includes our creatively named data property, status property, and error property. With these three properties, the child components can decide what states to render: 1. a loading state, 2. a loaded state with the rendered data, or 3. an error state.
  5. Since we have de-coupled the loading and managing of data from the UI components that are concerned about displaying it, we won’t have unnecessary data fetching when the UI components are mounted and un-mounted.
// src/components/DogDataProvider.tsxexport function useDogProviderState() {
const context = React.useContext(DogDataProviderContext);
if (context === undefined) {
throw new Error('useDogProviderState must be used within DogDataProvider.');
}
return context;
}
// src/index.tsx
function App() {
return (
<Router>
<div className="App">
{/* The data provder component responsible
for fetching and managing the data for the child components.
This needs to be at the top level of our component tree.*/}
<DogDataProvider>
<Nav />
<main className="py-5 md:py-20 max-w-screen-xl mx-auto text-center text-white w-full">
<Banner
title={'React Component Patterns:'}
subtitle={'Provider Pattern'}
/>
<Switch>
<Route exact path="/">
{/* A child component that will consume the data from
the data provider component, DogDataProvider. */}
<Profile />
</Route>
<Route path="/friends">
{/* A child component that will consume the data from
the data provider component, DogDataProvider. */}
<DogFriends />
</Route>
</Switch>
</main>
</DogDataProvider>
</div>
</Router>
);
}
const Profile = () => {
// Our custom hook that "subscirbes" to the state changes in
// the data provider component, DogDataProvider.
const { data, status, error } = useDogProviderState();
return (
<div>
<h1 className="//...">Profile</h1>
<div className="mt-10">
{/* If the API call returns an error we will show an error message */}
{error ? (
<Error errorMessage={error.message} />
// Show a loading state when we are fetching the data
) : status === Status.loading ? (
<Loader isInherit={true} />
) : (
// Display the content with the data
// provided via the custom hook, useDogProviderState.
<ProfileCard data={data} />
)}
</div>
</div>
);
};
  1. When fetching the data, we will show a loading state.
  2. If the API call returns an error, we will show an error message.
  3. Finally, we will render the component once the data is fetched and provided via the custom hook.

Conclusion

--

--

DhiWise enables developers to build web and mobile apps rapidly, with an intelligent platform. It combines people, technology and data in a single workflow. It helps companies solve the problem of resource scarcity by optimizing business resources with limited teams.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store