React components of design patterns

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 :





  1. RadioImageForm



    β€” , . , , , , (callback function prop), onStateChange



    . .





RadioImageForm



:





  1. 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:





  • React.Children.map





  • React.cloneElement





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>
  )
}
      
      



:





  1. RadioImageFormWrapper



    β€” . , CSS .





  2. React.children.map



    β€” , .





  3. 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



, . , .





  CodeSandBox





React (React hooks):





CodeSandBox






, , , 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';
      
      



  1. React.createContext



    , Provider Consumer. ; Provider .





  2. 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 , Β« Β», Β« Β».





CodeSandBox





React hooks:





CodeSandBox






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>
  );
};
      
      



:





  1. DogDataProviderContext



    React Context API React.createContext



    . React's (hook), .





  2. displayName



    , React Dev Tool. Context.Provider



    React Dev Tools DogDataProvider.Provider



    . , , Context.





  3. useEffect



    , .





  4. , . , : 1. , 2. 3. .





  5. (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>
  );
};
      
      



:





  1. .





  2. API , .





  3. , , , useDogProviderState



    , ProfileCard



    .





, . , , React-.





Provider





React React v16.8, v16.8, : CodeSandBox.






OTUS JavaScript . :





- JavaScript Developer. Basic





- JavaScript Developer. Professional





- React.js Developer








All Articles