How to use context to write high-performance react applications

Code Taoist 2022-06-23 15:28:01 阅读数:872

usecontextwritehigh-performancehigh

When you use Context when ,React Will render everything again for no reason ! Sometimes I have a feeling , The developers will Context As a magical elf , It will randomly and spontaneously re render the entire application for its own entertainment .

In this paper , I'm not going to convince anyone to give up their beloved state management library , Switch to Context. There are reasons for their existence . The main goal of this article is to uncover Context And provides some interesting coding patterns , This can help us minimize the risk of Context Relevant re rendering and improvement React Application performance .

stay React Implement a form in

Our tables will be very complicated , First , It includes :

  • “ Personal information ” part , People can set up some personal information in it , That's the name 、 E-mail, etc
  • “ Value calculation ” part , People can set their currency preferences in it 、 Their preferred discount 、 Add some coupons, etc
  • The discount selected should be highlighted in the personal section in the form of emoticons ( Don't ask , Designers have a strange sense of humor )
  • With operation buttons “ operation ” part ( namely “ preservation ”、“ Reset ” etc. )

Let's start with the component structure of the application . I know this form will soon become quite complicated , So I want to divide it into smaller ones right away 、 More contained components .

In the root directory there will be the main Form Components , It will present three required parts :

const Form = () => {
return (
<>
<PersonalInfoSection />
<ValueCalculationsSection />
<ActionsSection />
</>
);
};

“ Personal information ” Section will present three other components : Discount emoticons 、 Name input and country selection

const PersonalInfoSection = () => {
return (
<Section title="Personal information">
<DiscountSituation />
<NameFormComponent />
<SelectCountryFormComponent />
</Section>
);
};

All three of them will contain the actual logic of these components ( Their code will be shown below ), also Section It just encapsulates some styles .

“ Value calculation ” The section will have only one component ( at present ), Discount column :

const ValueCalculationSection = () => {
return (
<Section title="Value calculation">
<DiscountFormComponent />
</Section>
);
};

“Actions” Part now has only one button : with onSave Save button for callback .

const ActionsSection = ({ onSave }: { onSave: () => void }) => {
return (
<Section title="Actions">
<button onClick={onClick}>Save form</button>
</Section>
);
};

Now the interesting part comes : We need to make this form interactive . Consider that the entire form has only one “ preservation ” Button , Different parts require data from other parts , The natural place for state management is in Form In the root directory of the component . We will have... There 3 Data : name 、 Countries and discounts , A way to set all three types of data , And a kind of “ preservation ” Its method :

type State = {
name: string;
country: Country;
discount: number;
};
const Form = () => {
const [state, setState] = useState<State>(defaultState as State);
const onSave = () => {
// send the request to the backend here
};
const onDiscountChange = (discount: number) => {
setState({ ...state, discount });
};
const onNameChange = (name: string) => {
setState({ ...state, name });
};
const onCountryChange = (country: Country) => {
setState({ ...state, country });
};
// the rest as before
};

Now we need to pass the relevant data and callback to the component that needs it . In our PersonalInfoSection

  • DiscountSituation Components should be able to discount Values display emoticons .
  • NameFormComponent Should be able to control name value
  • SelectCountryFormComponent It should be possible to set the selected country

Consider that these components are not Form Directly rendered , It is PersonalInfoSection The child components , It's time to use prop 了 :

DiscountSituation Will accept discount As prop:

export const DiscountSituation = ({ discount }: { discount: number }) => {
// some code to calculate the situation based on discount
const discountSituation = ...;
return <div>Your discount situation: {discountSituation}</div>;
};

NameFormComponent Will accept name and onChange Callback :

export const NameFormComponent = ({ onChange, name }: { onChange: (val: string) => void; name: string }) => {
return (
<div>
Type your name here: <br />
<input onChange={() => onChange(e.target.value)} value={name} />
</div>
);
};

SelectCountryFormComponent Will accept onChange Callback :

export const SelectCountryFormComponent = ({ onChange }: { onChange: (country: Country) => void }) => {
return <SelectCountry onChange={onChange} />;
};

our PersonalInfoSection They must all be removed from the parent Form Component is passed to its child components :

export const PersonalInfoSection = ({
onNameChange,
onCountryChange,
discount,
name,
}: {
onNameChange: (name: string) => void;
onCountryChange: (name: Country) => void;
discount: number;
name: string;
}) => {
return (
<Section title="Personal information">
<DiscountSituation discount={discount} />
<NameFormComponent onChange={onNameChange} name={name} />
<SelectCountryFormComponent onChange={onCountryChange} />
</Section>
);
};

alike ,ValueCalculationSection Need to put onDiscountChange and discount Value from Form To component to its child components :

export const ValueCalculationsSection = ({ onDiscountChange }: { onDiscountChange: (val: number) => void }) => {
console.info('ValueCalculationsSection render');
return (
<Section title=" Value calculation ">
<DiscountFormComponent onDiscountChange={onDiscountChange} />
</Section>
);
};

also DiscountFormComponent Just use “ external ” library DraggingBar To render toolbars , And capture the changes through the callbacks it provides :

export const DiscountFormComponent = ({ onDiscountChange }: { onDiscountChange: (value: number) => void }) => {
console.info('DiscountFormComponent render');
return (
<div>
Please select your discount here :<br />
<DraggingBar onChange={(value: number) => onDiscountChange(value)} />
</div>
);
};

and , our Form The rendering of the component looks like this :

const Form = () => {
return (
<div>
<PersonalInfoSection onNameChange={onNameChange} onCountryChange={onCountryChange} discount={state.discount} name={state.name} />
<ValueCalculationsSection onDiscountChange={onDiscountChange} />
<ActionsSection onSave={onSave} />
</div>
);
};

Unfortunately , The result of several components and a simple state is much worse than you expected With CPU throttle , They are basically unusable . So what happened ?

Form performance survey

First , Let's look at the console output . If I type a key in the input Name, I will see :

Form render
PersonalInfoSection render
Section render
Discount situation render
NameFormComponent render
SelectCountryFormComponent render
ValueCalculationsSection render
Section render
DiscountFormComponent render
ActionsSection render
Section render

Each component in our form will be re rendered at each keystroke ! The same is true of dragging —— Every time the mouse moves , The entire form and all its components are re rendered . And we already know , SelectCountryFormComponent It's very slow , There is nothing we can do about its performance . So the only thing we can do here is to make sure it doesn't re render every time we press a button or move the mouse .

and , As we know , Components will be re rendered in the following cases :

  • Component state changes
  • Parent component re render

This is what happens here : When the value in the input changes , our Form The component propagates the value to the root component through a callback chain , There we change the root state , This triggers the component to re render Form, Then cascade to each child node and child node of the component ( That is, all child nodes ).

To solve this problem , Of course we can use useMemo and useCallback, But it just masks the problem , Did not really solve it . When we introduce another slow component in the future , There will still be the same problem . Not to mention that it makes the code more complex and difficult to maintain . In an ideal situation , When I was there Name When typing in a component , I just want to NameFormComponent And actual use name Re render components of values , The rest should just be idle and waiting to interact .

and React It actually provides us with a perfect tool to do this —— Context

Add... To the form Context

according to React file ,Context Provides a way to transfer data through component tree , Instead of manually passing down at each level prop. for example , If we were to Form State extraction to Context in , We can get rid of all that goes through the middle props.

To achieve this , First , We created Context Oneself , It will have our state and the API( That is, our callback ):

type State = {
name: string;
country: Country;
discount: number;
};
type Context = {
state: State;
onNameChange: (name: string) => void;
onCountryChange: (name: Country) => void;
onDiscountChange: (price: number) => void;
onSave: () => void;
};
const FormContext = createContext<Context>({} as Context);

Then we should put us in the component Form All state logic in moves to FormDataProvider In the component , And attach the state and callback to the newly created Context:

export const FormDataProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState<State>({} as State);
const value = useMemo(() => {
const onSave = () => {
// send the request to the backend here
};
const onDiscountChange = (discount: number) => {
setState({ ...state, discount });
};
const onNameChange = (name: string) => {
setState({ ...state, name });
};
const onCountryChange = (country: Country) => {
setState({ ...state, country });
};
return {
state,
onSave,
onDiscountChange,
onNameChange,
onCountryChange,
};
}, [state]);
return <FormContext.Provider value={value}>{children}</FormContext.Provider>;
};

Then expose the hook for other components to use Context Instead of accessing it directly :

 export const useFormState = () => useContext(FormContext);

And package our components Form To FormDataProvider

export default function App() {
return (
<FormDataProvider>
<Form />
</FormDataProvider>
);
}

after , We can get rid of the all prop, And pass useFormState The hook uses the required data and callbacks directly in the component that needs it .

for example , Our roots Form The component will look like this :

const Form = () => {
// no more props anywhere!
return (
<div className="App">
<PersonalInfoSection />
<ValueCalculationsSection />
<ActionsSection />
</div>
);
};

also NameFormComponent Will be able to access all data like this :

export const NameFormComponent = () => {
// accessing the data directly right where it's needed!
const { onNameChange, state } = useFormState();
const onValueChange = (e: ChangeEvent<HTMLInputElement>) => {
onNameChange(e.target.value);
};
return (
<div>
Type your name here: <br />
<input onChange={onValueChange} value={state.name} />
</div>
);
};

How about the performance of the new form ?

From a performance perspective , We haven't done that yet : Entering a name and dragging the bar still lags . however , If I start typing in the console NameFormComponent, I will now see :

Discount situation render
NameFormComponent render
SelectCountryFormComponent render
DiscountFormComponent render
ActionsSection render
Section render

Now half of the components will not be re rendered , Including our father Form Components . This happens because Context How it works : When Context When the value changes , this context Each user of will re render , Whether they use the changed value or not . and , Those who are Context Bypassed components do not re render at all .

Now? , If we look closely at our component implementation , especially SelectCountryComponent, We will find that it is not actually used state Oneself . All it needs is onCountryChange Callback :

export const SelectCountryFormComponent = () => {
const { onCountryChange } = useFormState();
console.info('SelectCountryFormComponent render');
return <SelectCountry onChange={onCountryChange} />;
};

This gives us a chance to try a very cool technique : We can separate state Part and API part .

Split state and API

Basically , What we need to do here is to put our “ whole ” The state is decomposed into two “ Micro state ”. Do not use one All inclusive context, Instead, use two contexts , One for data , One for the API:

type State = {
name: string;
country: Country;
discount: number;
};
type API = {
onNameChange: (name: string) => void;
onCountryChange: (name: Country) => void;
onDiscountChange: (price: number) => void;
onSave: () => void;
};
const FormDataContext = createContext<State>({} as State);
const FormAPIContext = createContext<API>({} as API);

We will pass the state directly to FormDataContext.Provider

const FormDataProvider = () => {
// state logic
return (
<FormAPIContext.Provider value={api}>
<FormDataContext.Provider value={state}>{children}</FormDataContext.Provider>
</FormAPIContext.Provider>
);
};

Now the most interesting part is api Value .

If we keep it the same , Whole “ decompose ” Your idea won't work , Because we still have to rely on hooks state Dependencies in :useMemo

const api = useMemo(() => {
const onDiscountChange = (discount: number) => {
// this is why we still need state here - in order to update it
setState({ ...state, discount });
};
// all other callbacks
return { onSave, onDiscountChange, onNameChange, onCountryChange };
// still have state as a dependency
}, [state]);

This will lead to api The value changes with each status update , And trigger re rendering each time the state is updated . We want us to api Of state remain unchanged , So the consumer won't re render .

Fortunately, , We can apply another clever technique here : Extract the state to setState in , Instead of calling a callback , We just need to trigger reducer operation .

First , establish action and reducer In itself :

type Actions =
| { type: 'updateName'; name: string }
| { type: 'updateCountry'; country: Country }
| { type: 'updateDiscount'; discount: number };
const reducer = (state: State, action: Actions): State => {
switch (action.type) {
case 'updateName':
return { ...state, name: action.name };
case 'updateDiscount':
return { ...state, discount: action.discount };
case 'updateCountry':
return { ...state, country: action.country };
}
};

Use reducer Instead of useState

export const FormProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(reducer, {} as State);
// ...
};

And put our api Migrate to dispatch instead of setState

export const FormProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(reducer, {} as State);
// ...
};

External state management

context It's no longer a mystery , Using these technologies , if necessary , You can easily write high-performance applications using pure context , If you want to switch to any other framework , You only need to make minimal changes to the code to complete . When you design your application context, State management framework Not important .

We might as well move it to the old one now Redux On . The only thing we need to do is : Get rid of Context and Providers, take React reducer Convert to Redux store, And put our hooks Convert to use Redux Selector and scheduling .

const store = createStore((state = {}, action) => {
switch (action.type) {
case 'updateName':
return { ...state, name: action.payload };
case 'updateCountry':
return { ...state, country: action.payload };
case 'updateDiscount':
return { ...state, discount: action.payload };
default:
return state;
}
});
export const FormDataProvider = ({ children }: { children: ReactNode }) => {
return <Provider store={store}>{children}</Provider>;
};
export const useFormDiscount = () => useSelector((state) => state.discount);
export const useFormCountry = () => useSelector((state) => state.country);
export const useFormName = () => useSelector((state) => state.name);
export const useFormAPI = () => {
const dispatch = useDispatch();
return {
onCountryChange: (value) => {
dispatch({ type: 'updateCountry', payload: value });
},
onDiscountChange: (value) => dispatch({ type: 'updateDiscount', payload: value }),
onNameChange: (value) => dispatch({ type: 'updateName', payload: value }),
onSave: () => {},
};
};

Everything else remains the same , And it works exactly according to our design .

版权声明:本文为[Code Taoist]所创,转载请带上原文链接,感谢。 https://qdmana.com/2022/174/202206231336145352.html