Compound Components
pattern is grouping related components together in a way so that it becomes easier to read and more extensible.
- When we want to leave state management to a parent/container/enclosing component and leave the markup to the the user of the components
- Extract the component that manages the state.
- Extract inner/user components and make them consume two things: 1- the parent/enclosing component's state by passing props from the parent via
Context
API (Example 1) or viarender props
method (Example 2). 2- A mechanism (usually a callback) to request for updating the state.
- The user of the component owns the markup. The implementation of the parent component doesn’t need a fixed markup structure. If using Context, you can do whatever you like, nest child component 10 levels deep and it will still work.
- The developer can rearrange the components in any order.
- The components don’t have to be jammed together explicitly, they can be written independently but they are still able to communicate via their parent component.
Compound components is a pattern in which components are used together such that they share an implicit state that lets them communicate with each other in the background.
Think of compound components like the <select> and <option> elements in HTML. Apart they don’t do too much, but together they allow you to create the complete experience. — Kent C. Dodds
It's cumbersome to pass props to all the nested levels of children, if not using Context i.e with cloning elements with props. With Context it's easily doable.
import React from 'react';
import TabSwitcher, { Tab, TabPanel } from './TabSwitcher';
function App() {
return (
<div className="App">
<h1>TabSwitcher with Compound Components</h1>
/* TabSwitcher keeps a shared state */
<TabSwitcher>
/* TabPanel and Tab consumes the shared state */
/* TabPanels adjust their render with the state */
<TabPanel whenActive="a">
<div>a panel</div>
</TabPanel>
<TabPanel whenActive="b">
<div>b panel</div>
</TabPanel>
/* Tabs update the state via a callback stored in the state object */
<Tab id="a">
<button>a</button>
</Tab>
<Tab id="b">
<button>b</button>
</Tab>
</TabSwitcher>
</div>
);
}
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
import React, { Component, createContext } from 'react';
const context = createContext({});
const { Provider, Consumer } = context;
const Tab = ({ id, children }) => (
<Consumer>
{({ changeTab }) => <div onClick={() => changeTab(id)}>{children}</div>}
</Consumer>
);
const TabPanel = ({ whenActive, children }) => (
<Consumer>
{({ activeTabId }) => (activeTabId === whenActive ? children : null)}
</Consumer>
);
class TabSwitcher extends Component {
state = {
activeTabId: 'a'
};
changeTab = newTabId => {
console.log(newTabId);
this.setState({
activeTabId: newTabId
});
};
render() {
return (
<Provider
value={{
activeTabId: this.state.activeTabId,
changeTab: this.changeTab
}}
>
{this.props.children}
</Provider>
);
}
}
export default TabSwitcher;
export { Tab, TabPanel };
function App() {
return (
<div>
{/* Wizard is state manager */}
<Wizard>
{/* Note how easily you can rearrange the components */}
<InputStep seq={0} field="name" label="Enter your name" />
<InputStep seq={1} field="number" label="Enter your mobile number" />
<InputStep seq={2} field="email" label="Enter your email" />
<Wizard.Buttons.Previous />
<Wizard.Buttons.Next />
<Wizard.Buttons.Submit seq={3} label="Submit" />
</Wizard>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
import React from "react";
import ReactDOM from "react-dom";
const InputStep = props => {
if (props.seq !== props.step) return null;
return (
<div>
<label>
{props.label}
<input
onChange={e => {
props.onChange({ [props.field]: e.target.value });
}}
/>
</label>
</div>
);
};
class Wizard extends React.Component {
// Static properties which are out components
static Buttons = {
Previous: props =>
props.canGoPrevious ? (
<button onClick={props.goPrevious}>Previous</button>
) : null,
Next: props =>
props.canGoNext ? <button onClick={props.goNext}>Next</button> : null,
Submit: props =>
props.canSubmit ? <input onClick={props.onSubmit} type="submit" /> : null
};
constructor(props) {
super(props);
this.state = { step: 0, userInfo: {} };
}
render() {
const lastStep = 2;
// Note how we are limited to the first level of children:
return React.Children.map(this.props.children, child => {
return React.cloneElement(child, {
step: this.state.step,
canGoNext: this.state.step < lastStep,
canGoPrevious: this.state.step > 0,
canSubmit: this.state.step === lastStep,
onChange: userInfoPart => {
this.setState(({ step, userInfo }) => ({
step,
userInfo: Object.assign(userInfo, userInfoPart)
}));
},
goPrevious: () => {
this.setState(({ step }) => ({ step: step - 1 }));
},
goNext: () => {
this.setState(({ step }) => ({ step: step + 1 }));
},
onSubmit: () => {
alert(JSON.stringify(this.state.userInfo));
}
});
});
}
}
Render Props
is a pattern to separate state management from rendering. Render props is the lowest level with regard to sharing logic.
- When we want to leave state management to a parent/container/enclosing component and leave the markup to the the user of the components and have a line of communication between.
Render props adds slightly more complexity comparing to Compound Component i.e extra work for user (you can compare the examples). So it fits a lot better where the rendering needs every fine detail and control over how things are rendered.
- Extract the component that manages the state.
- The child of the parent component is a function with the needed arguments to render themselves. Also, the mechanism to update the state is passed on by the parent.
- The parent component calls the child function (like
this.props.children(args)
with the arguments derived from the state. - No shared state anymore. The parent communicates with its child/children via callback.
- The user of the component has absolute control over the markup.
- The user of the parent component doesn't need to have access to any kind of
Context
. - Nesting wouldn't be an issue
- It's clear what props are going to be passed to the child components i.e it's not hidden in parent component implementation
import React from "react";
import ReactDOM from "react-dom";
const InputStep = props => {
return (
<div>
<label>
{props.label}
<input
onChange={e => {
props.onChange({ [props.field]: e.target.value });
}}
/>
</label>
</div>
);
};
class Wizard extends React.Component {
// Static properties which are out components
static Buttons = {
Previous: props => <button onClick={props.goPrevious}>Previous</button>,
Next: props => <button onClick={props.goNext}>Next</button>,
Submit: props => <input onClick={props.onSubmit} type="submit" />
};
constructor(props) {
super(props);
this.state = { step: 0, userInfo: {} };
}
render() {
const lastStep = 2;
return this.props.children({
step: this.state.step,
canGoNext: this.state.step < lastStep,
canGoPrevious: this.state.step > 0,
canSubmit: this.state.step === lastStep,
onChange: userInfoPart => {
this.setState(({ step, userInfo }) => ({
step,
userInfo: Object.assign(userInfo, userInfoPart)
}));
},
goPrevious: () => {
this.setState(({ step }) => ({ step: step - 1 }));
},
goNext: () => {
this.setState(({ step }) => ({ step: step + 1 }));
},
onSubmit: () => {
alert(JSON.stringify(this.state.userInfo));
}
});
}
}
function App() {
return (
<div>
{/* Wizard is state manager and consumes the following function */}
<Wizard>
{/* These are passed props. Note how we render the markup based on them*/}
{({
step,
canGoNext,
goNext,
canGoPrevious,
goPrevious,
canSubmit,
onChange,
onSubmit
}) => {
return (
<>
{step === 0 && (
<InputStep
onChange={onChange}
field="name"
label="Enter your name"
/>
)}
{step === 1 && (
<InputStep
onChange={onChange}
field="number"
label="Enter your mobile number"
/>
)}
{step === 2 && (
<InputStep
onChange={onChange}
field="email"
label="Enter your email"
/>
)}
{canGoPrevious && (
<Wizard.Buttons.Previous goPrevious={goPrevious} />
)}
{canGoNext && <Wizard.Buttons.Next goNext={goNext} />}
{canSubmit && (
<Wizard.Buttons.Submit onSubmit={onSubmit} label="Submit" />
)}
</>
);
}}
</Wizard>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Using Render Props
if we have a collection of props
that is common in the child component, Prop Collection
is the patter to use.
- The pattern is applied on top of
Render Props
. One good use case example is applying accessibility props. - Another use case is when container component needs you (user) to apply certain props for them so that they can wire up or trace things (example: autocomplete)
- The container component passes collection of props object to the child function. The responsibility of child components is to apply the props on themselves.
- Already mentioned in the intro.
import React from "react";
import ReactDOM from "react-dom";
const InputStep = ({ onChange, label, field, ...rest }) => {
return (
<div>
<label {...rest}>
{label}
<input
{...rest}
onChange={e => {
onChange({ [field]: e.target.value });
}}
/>
</label>
</div>
);
};
class Wizard extends React.Component {
// Static properties which are out components
static Buttons = {
Previous: props => <button onClick={props.goPrevious}>Previous</button>,
Next: props => <button onClick={props.goNext}>Next</button>,
Submit: props => <input onClick={props.onSubmit} type="submit" />
};
constructor(props) {
super(props);
this.state = { step: 0, userInfo: {} };
}
render() {
const lastStep = 2;
return this.props.children({
step: this.state.step,
canGoNext: this.state.step < lastStep,
canGoPrevious: this.state.step > 0,
canSubmit: this.state.step === lastStep,
accessibilityProps: {
"aria-expanded": true,
style: {
fontSize: "35px"
}
},
onChange: userInfoPart => {
this.setState(({ step, userInfo }) => ({
step,
userInfo: Object.assign(userInfo, userInfoPart)
}));
},
goPrevious: () => {
this.setState(({ step }) => ({ step: step - 1 }));
},
goNext: () => {
this.setState(({ step }) => ({ step: step + 1 }));
},
onSubmit: () => {
alert(JSON.stringify(this.state.userInfo));
}
});
}
}
function App() {
return (
<div>
{/* Wizard is state manager and consumes the following function */}
<Wizard>
{({
step,
canGoNext,
goNext,
canGoPrevious,
goPrevious,
// This is the common props you need to apply on all of your components
accessibilityProps,
canSubmit,
onChange,
onSubmit
}) => {
return (
<>
{step === 0 && (
<InputStep
{...accessibilityProps}
onChange={onChange}
field="name"
label="Enter your name"
/>
)}
{step === 1 && (
<InputStep
{...accessibilityProps}
onChange={onChange}
field="number"
label="Enter your mobile number"
/>
)}
{step === 2 && (
<InputStep
{...accessibilityProps}
onChange={onChange}
field="email"
label="Enter your email"
/>
)}
{canGoPrevious && (
<Wizard.Buttons.Previous goPrevious={goPrevious} />
)}
{canGoNext && <Wizard.Buttons.Next goNext={goNext} />}
{canSubmit && (
<Wizard.Buttons.Submit onSubmit={onSubmit} label="Submit" />
)}
</>
);
}}
</Wizard>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Using Prop Collections
we usually don't care what props we're getting back from the parent; we just apply them on the component. There's a chance that we accidentally override some of those props with props we set on the component. This pattern is to address this problem.
- Already mentioned in the intro.
- Similar to
Prop Collections
but instead of spreading an object, we actually call a function that accepts all the props that we want to apply to the components and the function's job is to compose those props with the common props.
- The consumer remains separated from the container and it never cares what props are going to be provided by the parent.
import React from "react";
import ReactDOM from "react-dom";
const InputStep = ({ seq, step, onChange, label, field, ...rest }) => {
return (
seq === step && (
<div>
<label {...rest}>
{label}
<input
{...rest}
onChange={e => {
onChange({ [field]: e.target.value });
}}
/>
</label>
</div>
)
);
};
class Wizard extends React.Component {
// Static properties which are out components
static Buttons = {
Previous: props =>
props.canGoPrevious && (
<button {...props} onClick={props.goPrevious}>
Previous
</button>
),
Next: props =>
props.canGoNext && (
<button {...props} onClick={props.goNext}>
Next
</button>
),
Submit: props =>
props.canSubmit && (
<input {...props} onClick={props.onSubmit} type="submit" />
)
};
constructor(props) {
super(props);
this.state = { step: 0, userInfo: {} };
}
render() {
const lastStep = 2;
const baseProps = {
step: this.state.step,
canGoNext: this.state.step < lastStep,
canGoPrevious: this.state.step > 0,
canSubmit: this.state.step === lastStep,
onChange: userInfoPart => {
this.setState(({ step, userInfo }) => ({
step,
userInfo: Object.assign(userInfo, userInfoPart)
}));
},
goPrevious: () => {
this.setState(({ step }) => ({ step: step - 1 }));
},
goNext: () => {
this.setState(({ step }) => ({ step: step + 1 }));
},
onSubmit: () => {
alert(JSON.stringify(this.state.userInfo));
},
style: {
fontSize: "25px"
}
};
const getProps = componentProps => {
if (!componentProps) return baseProps;
var result = { ...componentProps, ...baseProps };
if (componentProps.style) {
result.style = { ...componentProps.style, ...baseProps.style };
//Object.assign(result.style, componentProps.style);
}
return result;
};
return this.props.children({ getProps });
}
}
function App() {
return (
<div>
{/* Wizard is state manager and consumes the following function */}
<Wizard>
{/* getProps is the function Wizard provides and each component call with its own props to get a bigger props object to spread */}
{({ getProps }) => {
return (
<>
<InputStep
{...getProps({
seq: 0,
field: "name",
label: "Enter your name",
style: {
color: "blue"
}
})}
/>
<InputStep
{...getProps({
seq: 1,
field: "number",
label: "Enter your mobile number",
style: {
color: "orange"
}
})}
/>
<InputStep
{...getProps({
seq: 2,
field: "email",
label: "Enter your email",
style: {
color: "aqua"
}
})}
/>
<Wizard.Buttons.Previous
{...getProps({
style: {
color: "red"
}
})}
/>
<Wizard.Buttons.Next
{...getProps({
style: {
color: "limegreen"
}
})}
/>
<Wizard.Buttons.Submit
{...getProps({
seq: 2,
label: "Submit",
style: {
cursor: "pointer",
color: "green"
}
})}
/>
</>
);
}}
</Wizard>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Using State Initializer
, parent component provides a mechanism for the user to initialize its state. It works well with Render Props
pattern.
- Already mentioned in the intro.
- The parent exposes the mechanism to reset/initialize its state via a function
import React from "react";
import ReactDOM from "react-dom";
const InputStep = ({
userInfo,
seq,
step,
onChange,
label,
field,
...rest
}) => {
return (
seq === step && (
<div>
<label {...rest}>
{label}
<input
{...rest}
value={userInfo[field]}
onChange={e => {
onChange({ [field]: e.target.value });
}}
/>
</label>
</div>
)
);
};
class Wizard extends React.Component {
static Buttons = {
Previous: props =>
props.canGoPrevious && (
<button {...props} onClick={props.goPrevious}>
Previous
</button>
),
Next: props =>
props.canGoNext && (
<button {...props} onClick={props.goNext}>
Next
</button>
),
Submit: props =>
props.canSubmit && (
<input {...props} onClick={props.onSubmit} type="submit" />
),
Reset: props => (
<button {...props} onClick={props.onClick}>
Reset
</button>
)
};
constructor(props) {
super(props);
this.state = { step: 0, userInfo: {} };
}
render() {
const lastStep = 2;
const getProps = componentProps => {
const baseProps = {
userInfo: this.state.userInfo,
step: this.state.step,
canGoNext: this.state.step < lastStep,
canGoPrevious: this.state.step > 0,
canSubmit: this.state.step === lastStep,
onChange: userInfoPart => {
this.setState(({ step, userInfo }) => ({
step,
userInfo: Object.assign(userInfo, userInfoPart)
}));
},
goPrevious: () => {
this.setState(({ step }) => ({ step: step - 1 }));
},
goNext: () => {
this.setState(({ step }) => ({ step: step + 1 }));
},
onSubmit: () => {
alert(JSON.stringify(this.state.userInfo));
},
style: {
fontSize: "25px"
}
};
if (!componentProps) return baseProps;
var result = { ...componentProps, ...baseProps };
if (componentProps.style) {
result.style = { ...componentProps.style, ...baseProps.style };
}
return result;
};
// this is how the component initializes itself
const initialize = newUserInfo => {
this.setState({ userInfo: newUserInfo });
};
return this.props.children({ getProps, initialize });
}
}
function App() {
return (
<div>
{/* Wizard is state manager and consumes the following function */}
<Wizard>
{/* getProps is the function Wizard provides and each component call with its own props to get a bigger props object to spread */}
{({
getProps,
// this is the function to initialize the state
initialize
}) => {
return (
<>
<InputStep
{...getProps({
seq: 0,
field: "name",
label: "Enter your name",
style: {
color: "blue"
}
})}
/>
<InputStep
{...getProps({
seq: 1,
field: "number",
label: "Enter your mobile number",
style: {
color: "orange"
}
})}
/>
<InputStep
{...getProps({
seq: 2,
field: "email",
label: "Enter your email",
style: {
color: "aqua"
}
})}
/>
<Wizard.Buttons.Reset
{...getProps({
label: "Reset",
style: {
color: "red"
}
})}
onClick={() => {
// and we call it here
initialize({ name: "", age: "", email: "" });
}}
/>
<Wizard.Buttons.Previous
{...getProps({
style: {
color: "red"
}
})}
/>
<Wizard.Buttons.Next
{...getProps({
style: {
color: "limegreen"
}
})}
/>
<Wizard.Buttons.Submit
{...getProps({
seq: 2,
label: "Submit",
style: {
cursor: "pointer",
color: "green"
}
})}
/>
</>
);
}}
</Wizard>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
With State Reducer
enables users to control how the logic works i.e how the state is changed. This is similar to Render Props
where users control how things are rendered.
- When the user wants to take control of the another component's state or at least consult with a foreign component about it.
- The parent exposes the mechanism to broadcast its intention: changing state. In addition, it provides all the information that caller of the reducer function needs via an object called
Change Types
so that it can decide about the next state.
import React from "react";
import ReactDOM from "react-dom";
const InputStep = ({
userInfo,
seq,
step,
onChange,
label,
field,
digitOnly,
stateReducer,
...rest
}) => {
return (
seq === step && (
<div>
<div>{digitOnly && "Note: Enter only numbers"}</div>
<label {...rest}>
{label}
<input
{...rest}
value={userInfo[field]}
onChange={e => onChange({ [field]: e.target.value }, digitOnly)}
/>
</label>
</div>
)
);
};
class Wizard extends React.Component {
static Buttons = {
Previous: props =>
props.canGoPrevious && (
<button {...props} onClick={props.goPrevious}>
Previous
</button>
),
Next: props =>
props.canGoNext && (
<button {...props} onClick={props.goNext}>
Next
</button>
),
Submit: props =>
props.canSubmit && (
<input {...props} onClick={props.onSubmit} type="submit" />
),
Reset: props => (
<button {...props} onClick={props.onClick}>
Reset
</button>
)
};
static defaultUserInfo = {
name: "",
number: "+98",
email: ""
};
constructor(props) {
super(props);
this.state = { step: 0, userInfo: { ...Wizard.defaultUserInfo } };
Object.freeze();
}
// `internalSetState` replaces this.setState in the whole component.
// Here we consult with stateReducer. In addition to the nextState, we
// provide an object containing required info.
internalSetState(nextState, changeInfo = {}) {
this.setState(currentState =>
this.props.stateReducer(currentState, nextState, changeInfo)
);
}
render() {
const lastStep = 2;
const getProps = componentProps => {
const baseProps = {
userInfo: this.state.userInfo,
step: this.state.step,
canGoNext: this.state.step < lastStep,
canGoPrevious: this.state.step > 0,
canSubmit: this.state.step === lastStep,
onChange: (userInfoPart, digitOnly) => {
const newUserInfo = Object.assign(
{ ...this.state.userInfo },
userInfoPart
);
this.internalSetState(
{ userInfo: newUserInfo },
{ digitOnly, reason: "userInput" }
);
},
goPrevious: () => {
this.setState(({ step }) => ({ step: step - 1 }));
},
goNext: () => {
this.setState(({ step }) => ({ step: step + 1 }));
},
onSubmit: () => {
alert(JSON.stringify(this.state.userInfo));
},
style: {
fontSize: "25px"
}
};
if (!componentProps) return baseProps;
var result = { ...componentProps, ...baseProps };
if (componentProps.style) {
result.style = { ...componentProps.style, ...baseProps.style };
}
return result;
};
// this is how the component initializes itself
const initialize = newUserInfo => {
this.internalSetState({ userInfo: newUserInfo }, { reason: "force" });
};
return this.props.children({ getProps, initialize });
}
}
function App() {
return (
<div>
{/* Wizard is state manager and consumes the following function */}
<Wizard
// here we define state reducer and the function checks the change info passed in addition
// to the states.
// So for example, if it's a force change, then reducer is not needed to consult with
stateReducer={(currentState, nextState, { digitOnly, reason }) => {
if (
!reason ||
reason === "force" ||
!digitOnly ||
/^\+\d*$/.test(nextState.userInfo.number)
)
return nextState;
return currentState;
}}
>
{/* getProps is the function Wizard provides and each component call with its own props to get a bigger props object to spread */}
{({
getProps,
// this is the function to initialize the state
initialize
}) => {
return (
<>
<InputStep
{...getProps({
seq: 0,
field: "name",
label: "Enter your name",
style: {
color: "blue"
}
})}
/>
<InputStep
{...getProps({
seq: 1,
digitOnly: true,
field: "number",
label: "Enter your mobile number",
style: {
color: "orange"
}
})}
/>
<InputStep
{...getProps({
seq: 2,
field: "email",
label: "Enter your email",
style: {
color: "aqua"
}
})}
/>
<Wizard.Buttons.Reset
{...getProps({
label: "Reset",
style: {
color: "red"
}
})}
onClick={() => {
// and we call it here
initialize(Wizard.defaultUserInfo);
}}
/>
<Wizard.Buttons.Previous
{...getProps({
style: {
color: "red"
}
})}
/>
<Wizard.Buttons.Next
{...getProps({
style: {
color: "limegreen"
}
})}
/>
<Wizard.Buttons.Submit
{...getProps({
seq: 2,
label: "Submit",
style: {
cursor: "pointer",
color: "green"
}
})}
/>
</>
);
}}
</Wizard>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
React binding is one-way. A component takes its value via props
and broadcast a change to its value via onChange
callback. This pattern is exactly what's implemented in React controlled input
or form
elements.
The pattern is about where the source of truth is: inside component or outside. For example, when you create an input and set value on it like this:
<input value={this.state.someField} />
So we'll have two sources: the input's internal state and the state.someField
. In the mentioned case, as we have taken the control of the value of the input, the responsibility of updating the value is also on us. If we don't, the input behaves like a read-only element.
In contrast, if we did let the input element maintain its value, then we wouldn't need to do anything for value updating and hence we could only learn when the value changes.
In a controlled component, form data is handled by a React component. The alternative is uncontrolled components, where form data is handled by the DOM itself. To write an uncontrolled component, instead of writing an event handler for every state update, you can use a ref to get form values from the DOM.
A component can be controlled with respect to one value and uncontrolled with respect to another value.
- Hmmmm....
- The component checks the
props
for presence ofvalue
to realize if it's controlled or not. If it is, then on any change it doesn't update its internal state rather it suggests the change by callingonChange
callback and let the the function rejects or accepts the suggested value i.e updatesprops.value
which eventually update the value of the component.
import React, { useState } from "react";
import ReactDOM from "react-dom";
const SizeHolder = ({ size, onChange, initialValue }) => {
const [value, setValue] = useState(size);
const getValue = () => {
if (isControlled) {
return size;
} else {
return value || initialValue;
}
};
const isControlled = size !== undefined; // what if size equals 0 :)
return (
<input
value={getValue()}
onChange={e => {
const newSize = Number(e.target.value);
if (isControlled) {
onChange(newSize);
} else {
setValue(newSize);
}
}}
/>
);
};
const App = props => {
const ratio = 1.33333;
const [state, setState] = useState({
maintainRatio: true,
height: 600,
width: 800
});
return (
<div style={{ fontSize: "25px", lineHeight: "3em" }}>
<label>
Height:{" "}
<SizeHolder
size={state.height}
onChange={value => {
setState({
height: value,
width: Math.round(ratio * value),
maintainRatio: state.maintainRatio
});
}}
/>
</label>
<br />
<label>
<input
type="checkbox"
checked={state.maintainRatio}
onChange={value => {
setState({
height: state.height,
width: state.width,
maintainRatio: value.target.checked
});
}}
/>
Maintain aspact ration
</label>
<br />
Width
{!state.maintainRatio && (
// Uncontrolled component:
// - we don't care about onChange, and
// - the value of 'size' can be changed arbitrarily
<SizeHolder initialValue={state.width} />
)}
{state.maintainRatio && (
// Controlled component:
// - we handle onChange event to update the state, and
// - the value of 'size' cannot be changed arbitrarily
<SizeHolder
size={state.width}
onChange={width => {
setState({
width,
height: Math.round(width / ratio),
maintainRatio: state.maintainRatio
});
}}
/>
)}
<br />
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Provider
pattern is to address Props drilling
problem. It uses React Context API
. The pattern is based on Render props as the user needs to pass a function with the argument be Context object.
- When you want to pass down props to all levels yet you like to avoid
Props drilling
problem. Also, when you want to give your users absolute freedom.
The component holding state or information creates a Context
and keeps its provider for itself but exposes its consumer.
- Flexible
- Props drilling doesn't occur
- Implicit data flow
import React from "react";
import ReactDOM from "react-dom";
const WizardContext = React.createContext();
class Wizard extends React.Component {
static Consumer = WizardContext.Consumer;
static Buttons = {
Previous: props => (
<Wizard.Consumer>
{context =>
context.canGoPrevious && (
<button onClick={context.goPrevious} {...props} {...context}>
Previous
</button>
)
}
</Wizard.Consumer>
),
Next: props => (
<Wizard.Consumer>
{context =>
context.canGoNext && (
<button onClick={context.goNext} {...props} {...context}>
Next
</button>
)
}
</Wizard.Consumer>
),
Submit: props => (
<Wizard.Consumer>
{context =>
context.canSubmit && (
<button
onClick={context.onSubmit}
{...props}
{...context}
type="submit"
>
Submit
</button>
)
}
</Wizard.Consumer>
)
};
static defaultUserInfo = {
name: "",
number: "+98",
email: ""
};
constructor(props) {
super(props);
this.state = { step: 0, userInfo: { ...Wizard.defaultUserInfo } };
Object.freeze();
}
render() {
const lastStep = 2;
const baseProps = {
userInfo: this.state.userInfo,
step: this.state.step,
canGoNext: this.state.step < lastStep,
canGoPrevious: this.state.step > 0,
canSubmit: this.state.step === lastStep,
onChange: userInfoPart => {
this.setState(({ step, userInfo }) => ({
step,
userInfo: Object.assign(userInfo, userInfoPart)
}));
},
goPrevious: () => {
this.setState(({ step }) => ({ step: step - 1 }));
},
goNext: () => {
this.setState(({ step }) => ({ step: step + 1 }));
},
onSubmit: () => {
alert(JSON.stringify(this.state.userInfo));
},
style: {
fontSize: "25px",
color: "green"
}
};
const contextObject = { ...baseProps, userInfo: this.state.userInfo };
if (typeof this.props.children === "function") {
return this.props.children(contextObject);
}
return (
<WizardContext.Provider value={contextObject}>
{this.props.children}
</WizardContext.Provider>
);
}
}
const InputStep = ({ seq, label, field }) => (
<Wizard.Consumer>
{({ userInfo, step, onChange, ...rest }) => {
return (
seq === step && (
<div>
<label {...rest}>
{label}
<input
{...rest}
value={userInfo[field]}
onChange={e => onChange({ [field]: e.target.value })}
/>
</label>
</div>
)
);
}}
</Wizard.Consumer>
);
function App() {
return (
<div>
{/* Wizard is state manager */}
<Wizard>
{/* Note how easily you can rearrange the components */}
<InputStep seq={0} field="name" label="Enter your name" />
<InputStep seq={1} field="number" label="Enter your mobile number" />
<InputStep seq={2} field="email" label="Enter your email" />
<Wizard.Buttons.Previous />
<Wizard.Buttons.Next />
<Wizard.Buttons.Submit seq={3} label="Submit" />
</Wizard>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
HOC
is neither a component nor is higher order. It's a function that gets a component (either class or function) and returns a new one. It can return either a function or a class, but returning a class is not that usual. This is great for eliminating code duplication. It happens statically when your app boots up it creates all these components. HOC can always be replaced by render props, but sometimes it provides a nicer API. Sometimes you can provide both Render props and HOC.
- When you want to reuse a shared code across some components.
The wrapper component gets another component, does a various jobs like adding props, etc and return a new component. All of this happens one time. In contrast with render props that happens at render time where you have absolute control over whether or not applying certain props or not.
Note: The objective is that the consumer of HOC cannot tell that it is in fact higher order component. It should behave as close to possible as the component that it's wrapping.
Note: To forward from wrapper to wrapped component use ref
use React.forwardRef(wrappedComponentFunction)
import React from "react";
import ReactDOM from "react-dom";
// WithConsumer is a HOC. It takes a component and based off of the
// second argument applies certain props and decide whether to return
// anything or not. It uses Context to apply more properties.
const WithConsumer = (Component, { check, onClick } = {}) => {
console.log(Component.name);
const Wrapper = (props, ref) => {
return (
<Wizard.Consumer>
{context =>
(check === undefined || context[check]) && (
<Component
ref={ref}
{...context}
{...props}
onClick={context[onClick]}
/>
)
}
</Wizard.Consumer>
);
};
Wrapper.displayName = `WithConsumer(${Component.name})`;
// Note: 2 things:
// - If we have static propertied on wrapped component:
// return hoistNonReactStatics(React.forwardRef(Wrapper), Component);
// - The following line works perfectly except the Wrapper and Wrapped
// component will have different refs!
//return Wrapper;
return React.forwardRef(Wrapper);
};
const WizardContext = React.createContext();
class Wizard extends React.Component {
static Consumer = WizardContext.Consumer;
static Buttons = {
Previous: WithConsumer(props => <button {...props}>Previous</button>, {
check: "canGoPrevious",
onClick: "goPrevious"
}),
Next: WithConsumer(props => <button {...props}>Next</button>, {
check: "canGoNext",
onClick: "goNext"
}),
Submit: WithConsumer(
props => (
<button {...props} type="submit">
Submit
</button>
),
{
check: "canSubmit",
onClick: "onSubmit"
}
)
};
static defaultUserInfo = {
name: "",
number: "+98",
email: ""
};
constructor(props) {
super(props);
this.state = { step: 0, userInfo: { ...Wizard.defaultUserInfo } };
Object.freeze();
}
render() {
const lastStep = 2;
const baseProps = {
userInfo: this.state.userInfo,
step: this.state.step,
canGoNext: this.state.step < lastStep,
canGoPrevious: this.state.step > 0,
canSubmit: this.state.step === lastStep,
onChange: userInfoPart => {
this.setState(({ step, userInfo }) => ({
step,
userInfo: Object.assign(userInfo, userInfoPart)
}));
},
goPrevious: () => {
this.setState(({ step }) => ({ step: step - 1 }));
},
goNext: () => {
this.setState(({ step }) => ({ step: step + 1 }));
},
onSubmit: () => {
alert(JSON.stringify(this.state.userInfo));
},
style: {
fontSize: "25px",
color: "green"
}
};
const contextObject = { ...baseProps, userInfo: this.state.userInfo };
if (typeof this.props.children === "function") {
return this.props.children(contextObject);
}
return (
<WizardContext.Provider value={contextObject}>
{this.props.children}
</WizardContext.Provider>
);
}
}
const InputStep = ({ seq, label, field }) => (
<Wizard.Consumer>
{({ userInfo, step, onChange, ...rest }) => {
return (
seq === step && (
<div>
<label {...rest}>
{label}
<input
{...rest}
value={userInfo[field]}
onChange={e => onChange({ [field]: e.target.value })}
/>
</label>
</div>
)
);
}}
</Wizard.Consumer>
);
function App() {
return (
<div>
<Wizard>
<InputStep seq={0} field="name" label="Enter your name" />
<InputStep seq={1} field="number" label="Enter your mobile number" />
<InputStep seq={2} field="email" label="Enter your email" />
<Wizard.Buttons.Previous />
<Wizard.Buttons.Next />
<Wizard.Buttons.Submit seq={3} label="Submit" />
</Wizard>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
import React from "react";
import ReactDOM from "react-dom";
// This HOC returns a Class Component
const WithSuffocatingStyle = Component => SuffocatingComponent;
class SuffocatingComponent extends React.Component {
render() {
return ">>> Nothing Here. Suffocated <<< ";
}
}
// This HOC returns a Function Component
const WithStyle = Component => () => (
<>
WithStyle applied to:
<Component />
</>
);
function ComponentOne() {
return (
<div className="App">
<h1>C1</h1>
</div>
);
}
function ComponentTwo() {
return (
<div className="App">
<h1>C2 </h1>
</div>
);
}
const WithStyleComponentOne = WithStyle(ComponentOne);
const WithSuffocatingStyleComponentTwo = WithSuffocatingStyle(ComponentTwo);
const Compose = () => (
<>
<WithStyleComponentOne />
<br />
<WithSuffocatingStyleComponentTwo />
</>
);
const rootElement = document.getElementById("root");
ReactDOM.render(<Compose />, rootElement);