Skip to content

Instantly share code, notes, and snippets.

@Dizolivemint
Last active June 2, 2022 22:30
Show Gist options
  • Save Dizolivemint/16effe94f34dab9d5f327e016ce47704 to your computer and use it in GitHub Desktop.
Save Dizolivemint/16effe94f34dab9d5f327e016ce47704 to your computer and use it in GitHub Desktop.
React Component Design Patterns

Layout Components

Deal with arranging other components on a page

Split Screen

import styled from 'styled-components'

const Container = styled.div`
  display: flex;
`

const Pane = styled.div`
  flex: ${props => props.weight}
`
export const SplitScreen = ({
  children,
  leftWeight = 1,
  rightWeight = 1
}) => {
  const [left, right] = children
  return (
    <Container>
      <Pane weight={leftWeight}>
        {left}
      </Pane>
      <Pane weight={rightWeight}>
        {right}
      </Pane>
    </Container>
  )
}

Regular List

export const RegularList = ({
	items,
	resourceName,
	itemComponent: ItemComponent,
}) => {
	return (
		<>
		{items.map((item, i) => (
			<ItemComponent key={i} {...{ [resourceName]: item }} />
		))}
		</>
	)
}

Numbered List

export const NumberedList = ({
	items,
	resourceName,
	itemComponent: ItemComponent,
}) => {
	return (
		<>
		{items.map((item, i) => (
			<>
			<h3>{i + 1}</h3>
			<ItemComponent key={i} {...{ [resourceName]: item }} />
			</>
		))}
		</>
	)
}

Modal

import { useState } from 'react'

import styled from 'styled-components'

const ModalBackground = styled.div`
  position: fixed;
  z-index: 1;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: auto;
  background-color: rgba(0, 0, 0, 0.5);
`

const ModalBody = styled.div`
  background-color: white;
  margin: 10% auto;
  padding: 20px;
  width: 50%;
`

export const Modal = ({ children }) => {
  const [shouldShow, setShouldShow] = useState(false)

  return (
    <>
      <button onClick={() => setShouldShow(true)}>Show Modal</button>
      {shouldShow && (
        <ModalBackground onClick={() => setShouldShow(false)}>
          <ModalBody onClick={() => e.stopPropagation()}>
            <button onClick={() => setShouldShow(false)}>Hide Modal</button>
            {children}
          </ModalBody>
        </ModalBackground>
      )}
    </>
  )
}

Container Components

Data loading and management

Data Source

import React, { useState, useEffect } from 'react';
import axios from 'axios';

export const ResourceLoader = ({ resourceUrl, resourceName, children }) => {
	const [state, setState] = useState(null);

	useEffect(() => {
		(async () => {
			const response = await axios.get(resourceUrl);
			setState(response.data);
		})();
	}, [resourceUrl]);

	return (
		<>
		{React.Children.map(children, child => {
			if (React.isValidElement(child)) {
				return React.cloneElement(child, { [resourceName]: state });
			}

			return child;
		})}
		</>
	);
}

Resource Loader Parent

import axios from 'axios';
import { DataSource } from './DataSource';

const getServerData = url => async () => {
	const response = await axios.get(url);
	return response.data;
}

const getLocalStorageData = key => {
  return localStorage.getItem(key)
}

const Text = ({ message }) => <h1>{message}</h1>

function App() {
	return (
		<>
		<DataSource getDataFunc={getServerData('/users/123')} resourceName="user">
			<UserInfo />
		</DataSource>
    <DataSource getDataFunc={getLocalStorageData('message')} resourceName="message">
      <Text />
    </DataSource>
		</>
	);
}

export default App;

Resource Loader

import React, { useState, useEffect } from 'react';
import axios from 'axios';

export const ResourceLoader = ({ resourceUrl, resourceName, children }) => {
	const [state, setState] = useState(null);

	useEffect(() => {
		(async () => {
			const response = await axios.get(resourceUrl);
			setState(response.data);
		})();
	}, [resourceUrl]);

	return (
		<>
		{React.Children.map(children, child => {
			if (React.isValidElement(child)) {
				return React.cloneElement(child, { [resourceName]: state });
			}

			return child;
		})}
		</>
	);
}

User Info Component

export const UserInfo = ({ user }) => {
	const { name, age, hairColor, hobbies } = user || {};

	return user ? (
		<>
		<h3>{name}</h3>
		<p>Age: {age} years</p>
		<p>Hair Color: {hairColor}</p>
		<h3>Hobbies:</h3>
		<ul>
			{hobbies.map(hobby => <li key={hobby}>{hobby}</li>)}
		</ul>
		</>
	) : <p>Loading...</p>;
}

Product Info Component

export const ProductInfo = ({ product }) => {
	const { name, price, description, rating } = product || {};

	return product ? (
		<>
		<h3>{name}</h3>
		<p>{price}</p>
		<h3>Description:</h3>
		<p>{description}</p>
		<p>Average Rating: {rating}</p>
		</>
	) : <p>Loading...</p>;
}

Resource Loader Parent

import { ResourceLoader } from './ResourceLoader';
import { ProductInfo } from './ProductInfo';
import { UserInfo } from './UserInfo';

function App() {
	return (
		<>
      <ResourceLoader resourceUrl="/users/123" resourceName="user">
        <UserInfo />
      <ResourceLoader />
      <ResourceLoader resourceUrl="/products/1234" resourceName="product">
        <ProductInfo />
      <ResourceLoader />
		</>
	);
}

export default App;

Uncontrolled Components

Component keeps track of internal state and only releases data when some event occurs

Uncontrolled Form

export const UncontrolledForm = () => {
	const nameInput = React.createRef();
	const ageInput = React.createRef();
	const hairColorInput = React.createRef();

	const handleSubmit = e => {
		console.log(nameInput.current.value);
		console.log(ageInput.current.value);
		console.log(hairColorInput.current.value);
		e.preventDefault();
	}

	return (
		<form onSubmit={handleSubmit}>
			<input name="name" type="text" placeholder="Name" ref={nameInput} />
			<input name="age" type="number" placeholder="Age" ref={ageInput} />
			<input name="hairColor" type="text" placeholder="Hair Color" ref={hairColorInput} />
			<input type="submit" value="Submit" />
		</form>
	);
}

Uncontrolled Modal

import { useState } from 'react';
import styled from 'styled-components';

const ModalBackground = styled.div`
	position: fixed;
	z-index: 1;
	left: 0;
	top: 0;
	width: 100%;
	height: 100%;
	overflow: auto;
	background-color: rgba(0, 0, 0, 0.5);
`;

const ModalBody = styled.div`
	background-color: white;
	margin: 10% auto;
	padding: 20px;
	width: 50%;
`;

export const UncontrolledModal = ({ children }) => {
	const [shouldShow, setShouldShow] = useState(false);

	return (
		<>
		<button onClick={() => setShouldShow(true)}>Show Modal</button>
		{shouldShow && (
			<ModalBackground onClick={() => setShouldShow(false)}>
				<ModalBody onClick={e => e.stopPropagation()}>
					<button onClick={() => setShouldShow(false)}>Hide Modal</button>
					{children}
				</ModalBody>
			</ModalBackground>
		)}
		</>
	);
}

Uncontrolled Onboarding Flow

import React, { useState } from 'react';

export const UncontrolledOnboardingFlow = ({ children, onFinish }) => {
	const [onboardingData, setOnboardingData] = useState({});
	const [currentIndex, setCurrentIndex] = useState(0);

	const goToNext = stepData => {
		const nextIndex = currentIndex + 1;

		const updatedData = {
			...onboardingData,
			...stepData,
		};

		console.log(updatedData);

		if (nextIndex < children.length) {
			setCurrentIndex(nextIndex);
		} else {
			onFinish(updatedData);
		}

		setOnboardingData(updatedData);
	}

	const currentChild = React.Children.toArray(children)[currentIndex];

	if (React.isValidElement(currentChild)) {
		return React.cloneElement(currentChild, { goToNext });
	}

	return currentChild;
}

Parent Onboarding Flow

import { UncontrolledOnboardingFlow } from './UncontrolledOnboardingFlow';

const StepOne = ({ goToNext }) => (
	<>
	<h1>Step 1</h1>
	<button onClick={() => goToNext({ name: 'John Doe' })}>Next</button>
	</>
);
const StepTwo = ({ goToNext }) => (
	<>
	<h1>Step 2</h1>
	<button onClick={() => goToNext({ age: 100 })}>Next</button>
	</>
);
const StepThree = ({ goToNext }) => (
	<>
	<h1>Step 3</h1>
	<button onClick={() => goToNext({ hairColor: 'brown' })}>Next</button>
	</>
);

function App() {
	return (
		<UncontrolledOnboardingFlow onFinish={data => {
			console.log(data);
			alert('Onboarding complete!');
		}}>
			<StepOne />
			<StepTwo />
			<StepThree />
		</UncontrolledOnboardingFlow>
	);
}

export default App;

Controlled Components

Component does not keep track of internal state and passes data with props

Controlled Form

import { useState, useEffect } from 'react';

export const ControlledForm = () => {
	const [nameInputError, setNameInputError] = useState('');
	const [name, setName] = useState('');
	const [age, setAge] = useState();
	const [hairColor, setHairColor] = useState('');

	useEffect(() => {
		if (name.length < 2) {
			setNameInputError('Name must be two or more characters');
		} else {
			setNameInputError('');
		}
	}, [name])

	return (
		<form>
			{nameInputError && <p>{nameInputError}</p>}
			<input
				name="name"
				type="text"
				placeholder="Name"
				value={name}
				onChange={e => setName(e.target.value)} />
			<input
				name="age"
				type="number"
				placeholder="Age"
				value={age}
				onChange={e => setAge(Number(e.target.value))} />
			<input
				name="hairColor"
				type="text"
				placeholder="Hair Color"
				value={hairColor}
				onChange={e => setHairColor(e.target.value)} />
			<button>Submit</button>
		</form>
	)
}

Controlled Modal

import styled from 'styled-components';

const ModalBackground = styled.div`
	position: fixed;
	z-index: 1;
	left: 0;
	top: 0;
	width: 100%;
	height: 100%;
	overflow: auto;
	background-color: rgba(0, 0, 0, 0.5);
`;

const ModalBody = styled.div`
	background-color: white;
	margin: 10% auto;
	padding: 20px;
	width: 50%;
`;

export const ControlledModal = ({ shouldShow, onRequestClose, children }) => {
	return shouldShow ? (
		<ModalBackground onClick={onRequestClose}>
			<ModalBody onClick={e => e.stopPropagation()}>
				<button onClick={onRequestClose}>Hide Modal</button>
				{children}
			</ModalBody>
		</ModalBackground>
	) : null;
}

Controlled Onboarding Flow

export const ControlledOnboardingFlow = ({ children, onFinish, currentIndex, onNext }) => {
  const goToNext = stepData => {
    onNext()
  }

	const currentChild = React.Children.toArray(children)[currentIndex];

	if (React.isValidElement(currentChild)) {
		return React.cloneElement(currentChild, { goToNext });
	}

	return currentChild;
}

Parent of Onboarding Flow

import { useState } from 'react';
import { ControlledOnboardingFlow } from './ControlledOnboardingFlow';

const StepOne = ({ goToNext }) => (
	<>
	<h1>Step 1</h1>
	<button onClick={() => goToNext({ name: 'John Doe' })}>Next</button>
	</>
);
const StepTwo = ({ goToNext }) => (
	<>
	<h1>Step 2</h1>
	<button onClick={() => goToNext({ age: 100 })}>Next</button>
	</>
);
const StepThree = ({ goToNext }) => (
	<>
	<h1>Step 3</h1>
  <p>Congratulations! You qualify for our senior discount</p>
	<button onClick={() => goToNext({})}>Next</button>
	</>
);
const StepFour = ({ goToNext }) => (
	<>
	<h1>Step 4</h1>
	<button onClick={() => goToNext({ hairColor: 'brown' })}>Next</button>
	</>
);

function App() {
  const [onboardingData, setOnboardingData] = useState({})
  const [currentIndex, setCurrentIndex] = useState(0)

  const onNext = stepData => {
    setOnboardingData({ ...onboardingData, ...stepData });
    setCurrentIndex(nextIndex + 1);
  }

	return (
		<ControlledOnboardingFlow
			currentIndex={currentIndex}
      onNext={onNext}
		>
			<StepOne />
			<StepTwo />
			{onboardingData.age >= 62 && <StepThree />}
      <StepFour />
		</ControlledOnboardingFlow>
	);
}

export default App;

Higher-Order Components

A component that returns another component instead of JSX

Printing Props

export const printProps = Component => {
	return (props) => {
		console.log(props);

		return <Component {...props} />
	}
}

Loading Data

import React, { useState, useEffect } from 'react';
import axios from 'axios';

export const withUser = (Component, userId) => {
	return props => {
		const [user, setUser] = useState(null);

		useEffect(() => {
			(async () => {
				const response = await axios.get(`/users/${userId}`);
				setUser(response.data);
			})();
		}, []);

		return <Component {...props} user={user} />
	}
}

User info component for HOC

export const UserInfo = ({ user }) => {
	const { name, age, hairColor, hobbies } = user || {};

	return user ? (
		<>
		<h3>{name}</h3>
		<p>Age: {age} years</p>
		<p>Hair Color: {hairColor}</p>
		<h3>Hobbies:</h3>
		<ul>
			{hobbies.map(hobby => <li key={hobby}>{hobby}</li>)}
		</ul>
		</>
	) : <p>Loading...</p>;
}

Modifying Data (User specific)

import React, { useState, useEffect } from 'react';
import axios from 'axios';

export const withEditableUser = (Component, userId) => {
	return props => {
		const [originalUser, setOriginalUser] = useState(null);
		const [user, setUser] = useState(null);

		useEffect(() => {
			(async () => {
				const response = await axios.get(`/users/${userId}`);
				setOriginalUser(response.data);
				setUser(response.data);
			})();
		}, []);

		const onChangeUser = changes => {
			setUser({ ...user, ...changes });
		}

		const onSaveUser = async () => {
			const response = await axios.post(`/users/${userId}`, { user });
			setOriginalUser(response.data);
			setUser(response.data);
		}

		const onResetUser = () => {
			setUser(originalUser);
		}

		return <Component {...props}
			user={user}
			onChangeUser={onChangeUser}
			onSaveUser={onSaveUser}
			onResetUser={onResetUser} />
	}
}

Forms with HOC

import { withEditableUser } from "./withEditableUser";

export const UserInfoForm = withEditableUser(({ user, onChangeUser, onSaveUser, onResetUser }) => {
	const { name, age, hairColor } = user || {};

	return user ? (
		<>
		<label>
			Name:
			<input value={name} onChange={e => onChangeUser({ name: e.target.value })} />
		</label>
		<label>
			Age:
			<input type="number" value={age} onChange={e => onChangeUser({ age: Number(e.target.value) })} />
		</label>
		<label>
			Hair Color:
			<input value={hairColor} onChange={e => onChangeUser({ hairColor: e.target.value })} />
		</label>
		<button onClick={onResetUser}>Reset</button>
		<button onClick={onSaveUser}>Save Changes</button>
		</>
	) : <p>Loading...</p>;
}, '123');

Modifying Data (Any)

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1)
export const withEditableResource = (Component, resourcePath, resourceName) => {
	return props => {
		const [originalUser, setOriginalData] = useState(null);
		const [data, setData] = useState(null);

		useEffect(() => {
			(async () => {
				const response = await axios.get(resourcePath);
				setOriginalData(response.data);
				setData(response.data);
			})();
		}, []);

		const onChange = changes => {
			setData({ ...data, ...changes });
		}

		const onSave = async () => {
			const response = await axios.post(resourcePath, { [resourceName]: data });
			setOriginalData(response.data);
			setData(response.data);
		}

		const onReset = () => {
			setData(originalData);
		}

    const resourceProps = {
      [resourceName]: data,
      [`onChange${capitalize}`]: onChange,
      [`onSave${capitalize}`]: onSave,
      [`onReset${capitalize}`]: onReset,
    }

		return <Component {...props} {...resourceProps} />
	}
}

Form with Resource Component

resourcePath and resourceName have to be passed and new import (otherwise the same).

import { withEditableResource } from "./withEditableResource";

export const UserInfoForm = withEditableResource(({ user, onChangeUser, onSaveUser, onResetUser }) => {
	const { name, age, hairColor } = user || {};

	return user ? (
		<>
		<label>
			Name:
			<input value={name} onChange={e => onChangeUser({ name: e.target.value })} />
		</label>
		<label>
			Age:
			<input type="number" value={age} onChange={e => onChangeUser({ age: Number(e.target.value) })} />
		</label>
		<label>
			Hair Color:
			<input value={hairColor} onChange={e => onChangeUser({ hairColor: e.target.value })} />
		</label>
		<button onClick={onResetUser}>Reset</button>
		<button onClick={onSaveUser}>Save Changes</button>
		</>
	) : <p>Loading...</p>;
}, '/users/123', 'user');

Custom Hook Patterns

Special hooks we define ourselves, and that usually combine the functionality of one or more existing React hooks like "useState" or "useEffect"

useCurrentUser Hook

import { useState, useEffect } from 'react';
import axios from 'axios';

export const useCurrentUser = () => {
	const [user, setUser] = useState(null);

	useEffect(() => {
		(async () => {
			const response = await axios.get('/current-user');
			setUser(response.data);
		})();
	}, []);

	return user;
}

useUser Hook

import { useState, useEffect } from 'react';
import axios from 'axios';

export const useUser = userId => {
	const [user, setUser] = useState(null);

	useEffect(() => {
		(async () => {
			const response = await axios.get(`/users/${userId}`);
			setUser(response.data);
		})();
	}, [userId]);

	return user;
}

useResource Hook

import { useState, useEffect } from 'react';
import axios from 'axios';

export const useResource = resourceUrl => {
	const [resource, setResource] = useState(null);

	useEffect(() => {
		(async () => {
			const response = await axios.get(resourceUrl);
			setResource(response.data);
		})();
	}, [resourceUrl]);

	return resource;
}

useDataSource Hook

import { useState, useEffect } from 'react';
import axios from 'axios';

export const useDataSource = getResourceFunc => {
	const [resource, setResource] = useState(null);

	useEffect(() => {
		(async () => {
			const result = await getResourceFunc();
			setResource(result.data);
		})();
	}, [resourceUrl]);

	return resource;
}

Functional Programming HOC

Recursive Components

const isObject = x => typeof x === 'object' && x !== null;

export const RecursiveComponent = ({ data }) => {
	if (!isObject(data)) {
		return (
			<li>{data}</li>
		);
	}

	const pairs = Object.entries(data);

	return (
		<>
		{pairs.map(([key, value]) => (
			<li>
				{key}:
				<ul>
					<RecursiveComponent data={value} />
				</ul>
			</li>
		))}
		</>
	);
}

Composition Components

export const Button = ({ size, color, text, ...props }) => {
	return (
		<button style={{
			padding: size === 'large' ? '32px' : '8px',
			fontSize: size === 'large' ? '32px' : '16px',
			backgroundColor: color,
		}} {...props}>{text}</button>
	);
}

export const DangerButton = props => {
	return (
		<Button {...props} color="red" />
	);
}

export const BigSuccessButton = props => {
	return (
		<Button {...props} size="large" color="green" />
	);
}

Partially Applied Component

export const partiallyApply = (Component, partialProps) => {
  return props => {
    return <Component {...partialProps} {...props} />
  }
}

export const Button = ({ size, color, text, ...props }) => {
	return (
		<button style={{
			padding: size === 'large' ? '32px' : '8px',
			fontSize: size === 'large' ? '32px' : '16px',
			backgroundColor: color,
		}} {...props}>{text}</button>
	);
}

export const DangerButton = partiallyApply(Button, { color: 'red' })
export const BigSuccessButton = partiallyApply(Button, { color: 'green', size: 'large' })
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment