Introduction
(patterns) React, , . , , , DRY (Donβt repeat yourself - ) . , React , (prop drilling) . , CodeSandBox.
, , .
β , , (implicit state), . , .
, <select> <option> HTML. , , . ( . )
? ?
, : , . . ; , , . , , .
. , , , , . CodeSandBox.
(parent component), RadioImageForm
, , , "", RadioInput,
. .
{/* 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>
src/components/RadioImageForm.tsx
1 :
RadioImageForm
β , . , , , , (callback function prop),onStateChange
. .
RadioImageForm
:
RadioInput
β ,RadioImageForm
.RadioInput
β , , ,<RadioImageForm.RadioInput/>
. ,RadioInput
.
RadioInput
RadioImageForm
.RadioImageForm
RadioInput
. «» ( "sub-components.").
RadioImageForm
.
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>
)
}
}
, , . RadioInput
, onChange
, «» (user's props). ? React.children.map
React.cloneElement
. , React:
RadioImageForm
:
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>
)
}
:
RadioImageFormWrapper
β . , CSS .
React.children.map
β , .
React.cloneElement
β React docs:
React- , . (props) , . .
React.children.map
React.cloneElement
. , , . RadioImageForm
RadioInput
. React.cloneElement
, , RadioInput, .
, RadioInput
RadioImageForm
. , RadioInput
, RadioImageForm
. . . RadioInput
:
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>
);
,
RadioInputProps
,RadioInput
.
RadioInput
(dot-syntax notation) (RadioImageForm.RadioInput
):
// 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>
RadioInput
,RadioImageForm
. , ,RadioImageForm
. ,this.onChange
:static RadioInput = () => <input onChange={this.onChange} //β¦
. , . , RadioImageForm
, . RadioInput
.
. , . RadioImageForm
, , , .
, . <RadioImageForm.RadioInput/>
div-? , ? , RadioImageForm
, . , .
React (React hooks):
, , , div-? . . , .
? ?
, , , . β , . , . React's Context API.
(context) React's Context API, React docs.
RadioImageForm . CodeSandBox.
RadioImageForm
, (, RadioInput
) . , React's Context, React's doc:
.
-, React.createContext
, . . RadioImageForm.tsx
.
const RadioImageFormContext = React.createContext({
currentValue: '',
defaultValue: undefined,
onChange: () => { },
});
RadioImageFormContext.displayName = 'RadioImageForm';
React.createContext
, Provider Consumer. ; Provider .
displayName
, React Dev Tool (React Developer Tools). ,Context.Provider
Context.Consumer
RadioImageForm.Provider
RadioImageForm.Consumer
. , , Context .
- RadioImageForm
React.children.map
React.cloneElement
, .
render(): React.ReactElement {
const { children } = this.props;
return (
<RadioImageFormWrapper>
<RadioImageFormContext.Provider value={this.state}>
{children}
</RadioImageFormContext.Provider>
</RadioImageFormWrapper>
);
}
RadioImageFormContext.Provider
(prop-) value
. , β , (descendants) Provider. , onChange
. onChange
, currentValue
defaultValue
state
, this.state
.
? , value - , . React - , , value, (re-render) , ( ). , value , (re-renders) , . : <RadioImageFormContext.Provider value={{ currentValue: this.state.currentValue, onChange: this.onChange }}>. this.state, .
, , , , . RadioImageForm
, Consumer RadioImageForm
.
export class RadioImageForm extends React.Component<Props, State> {
static Consumer = RadioImageFormContext.Consumer;
//...
, , ,
RadioImageFormContext.Consumer
, ,const RadioImageFormConsumer = RadioImageFormContext.Consumer
.
Consumer , .
, , , currentValue
, . RadioImageForm
SubmitButton
.
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>
);
, Consumer ; render props, ({ currentValue }) => (// Render content))
. , . , Provider. , SubmitButton
currentValue
, RadioImageForm
. Context.
, ( ), React Docs.
(compound components) (component tree).
src/index.tsx , .
, , . , . Context API . . Flexible Compound Components: . API , Β« Β», Β« Β».
React hooks:
Provider
(provider pattern) React. , β API React .
React docs on Context API Render Props.
Context API:
.
(Render Props):
"render prop" React-, (prop), .
(provider pattern)? ?
β , , . React , (prop drill) . - (spaghetti code).
, . React's Context API , . , , , , . β . , . , (), , , . - (normalizing) (massaging) (response data), API (response data model). , . .
, , Redux, MobX, Recoil, Rematch, Unstated, Easy Peasy, ? , . , , , , . , , , , , . React, , , React. , (codebase) , , , . .
, . , , , , (SoC) DRY (Don't Repeat Yourself). CodeSandBox. , , .
, DogDataProvider
, , , API React's Context.
// 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>
);
};
:
DogDataProviderContext
React Context APIReact.createContext
. React's (hook), .
displayName
, React Dev Tool.Context.Provider
React Dev ToolsDogDataProvider.Provider
. , , Context.
useEffect
, .
, . , : 1. , 2. 3. .
(UI), , , .
React hook , DogDataProvider
. (hook) DogDataProvider
.
// src/components/DogDataProvider.tsx
export function useDogProviderState() {
const context = React.useContext(DogDataProviderContext);
if (context === undefined) {
throw new Error('useDogProviderState must be used within DogDataProvider.');
}
return context;
}
[React.useContext](https://reactjs.org/docs/hooks-reference.html#usecontext)
DogDataProvider
, , . , - , .
, , . , .
, . Profile, (home path), DogFriends
Nav .
index.tsx DogDataProvider
(root level):
// 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>
);
}
Profile
useDogProviderState
:
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>
);
};
:
.
API , .
, , ,
useDogProviderState
,ProfileCard
.
, . , , React-.
React React v16.8, v16.8, : CodeSandBox.
OTUS JavaScript . :
- JavaScript Developer. Professional