Front-end development often involves the handling of forms, or some other component used for input, such as a calendar component.


When it comes to inputs, there is no getting around the concept of controlled and uncontrolled modes.

 What is controlled and what is uncontrolled?

 Think about it, there are only two ways to change a form value:


The user changes the value or the code changes the value.


If you can’t change the form value value through code, then it’s uncontrolled, i.e., it’s not under our control.


But the code can set the initial value defaultValue to the form.


The code sets the initial value of the form, but the only person who can change the value is the user. The code listens to onChange to get the latest value, or it can get the dom through a ref and read the value.

 This would be the uncontrolled mode.


In turn, the code can change the value of the form, which is the controlled mode.


Note that value is not the same as defaultValue:


defaultValue will be used as the initial value of the value, which will be changed by the user later.


And once you set the value to the input, the user can’t change it, the input can trigger the onChange event, but the value of the form won’t change.


After the user inputs, we get the input in the onChange event and set the value in the code.

 This is the controlled mode.


Actually, in the vast majority of cases, uncontrolled is fine, since we’re just going to get the user’s input and don’t need to manually modify the form values.


But there are times when you need to do some processing based on the user’s input and then set it to the value of the form, and this kind of needs controlled mode.


Or you can use controlled mode when you want to synchronize the values of a form to another place, similar to the Form component.


value Controlled by the user is uncontrolled mode, controlled by the code is controlled mode.

 Let’s write code to try it out:

npx create-vite


Create a vite + react project.


Remove index.css and StrictMode from main.tsx:

 Change App.tsx

import { ChangeEvent } from "react"

function App() {

  function onChange(event: ChangeEvent<HTMLInputElement>) {
    console.log(event.target.value);
  }

  return <input defaultValue={'guang'} onChange={onChange}/>
}

export default App

 Run a little development service:

npm install
npm run dev

 Look at the effect:


The defaultValue is used as the initial value of the value, and then the user input triggers the onChange event, which gets the value via event.target.


Of course, uncontrolled mode doesn’t necessarily get the latest value via onChange, but also via ref.

import { useEffect, useRef } from "react"

function App() {

  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    setTimeout(() => {
      console.log(inputRef.current?.value);
    }, 2000);
  }, []);

  return <input defaultValue={'guang'} ref={inputRef}/>
}

export default App

 Next look at the controlled mode writeup:

import { ChangeEvent, useState } from "react"

function App() {

  const [value, setValue] = useState('guang');

  function onChange(event: ChangeEvent<HTMLInputElement>) {
    console.log(event.target.value);
    // setValue(event.target.value);
  }

  return <input value={value} onChange={onChange}/>
}

export default App


Let’s comment out setValue and see if the user can change it:


As you can see, the user can type, and onChange can get the input form value, but the value doesn’t change.

 Remove the comment on the setValue line and you’re good to go.

 Although functionally similar, this writing style is not recommended:


You do not let the user control, but through the code control, around the circle results did not change the value of the value, or the original, Figure what?


And controlled mode causes the component to re-render every time it setsValue.

 Try it:


Each input will setValue, which then triggers the component to re-render:

 And uncontrolled mode will only render once:


You’ve gone around in circles without changing anything, and caused a lot of re-rendering of components, so what are you trying to do with Controlled Mode?

 And what situations use controlled mode?


Of course it’s when you need to set the input values to the form after processing them, or when you want to synchronize the state values to the parent component in real time.

 For example, change user input to uppercase:

import { ChangeEvent, useState } from "react"

function App() {

  const [value, setValue] = useState('guang');

  function onChange(event: ChangeEvent<HTMLInputElement>) {
    console.log(event.target.value)
    setValue(event.target.value.toUpperCase());
  }

  return <input value={value} onChange={onChange}/>
}

export default App


In this case, you need to change the user input and set the value.

 But such scenarios are actually rare.


Some students might say Form components, and indeed, form items wrapped with Form.Item are controlled components:


Indeed, that’s because there’s a Store within the Form component that will synchronize the form values over and then centrally manage and set the values:


But also, because they are all controlled components, the form re-renders many times as the user enters, and performance can be bad.


If you’re using a standalone component, such as a Calendar, there’s no need to use controlled mode, just use uncontrolled mode and set the defaultValue.


A lot of people come up and set value, then listen for onChange, but then go around and convert the user input to value as is.

 Doesn’t make much sense and flat out causes many re-renders of the component.


In addition to native form elements, components also need to consider controlled and uncontrolled scenarios.

 For example, the calendar component:


Its parameters will have to be considered whether to support defaultValue in uncontrolled mode or use value + onChange in controlled mode.


If this is a business component, it’s basically a defaultValue in uncontrolled mode, where the caller just gets the user’s input.


Trigger additional rendering with the value of the controlled mode and also setValue.


But you can’t do that with base components, you have to support them all and let the caller choose.


The Calendar component of ant design does just that:

 ColorPicker component as well:

 It supports both controlled and uncontrolled components.

 How do you do that?

 Let’s try it:

 First write the uncontrolled component writeup:

import { ChangeEvent, useState } from "react"

interface CalendarProps{
  defaultValue?: Date;
  onChange?: (date: Date) => void;
}
function Calendar(props: CalendarProps) {
  
  const {
    defaultValue = new Date(),
    onChange
  } = props;

  const [value, setValue] = useState(defaultValue);

  function changeValue(date: Date) {
    setValue(date);
    onChange?.(date);
  } 


  return <div>
    {value.toLocaleDateString()}
    <div onClick={()=> {changeValue(new Date('2024-5-1'))}}>2023-5-1</div>
    <div onClick={()=> {changeValue(new Date('2024-5-2'))}}>2023-5-2</div>
    <div onClick={()=> {changeValue(new Date('2024-5-3'))}}>2023-5-3</div>
  </div>
}

function App() {
  return <Calendar defaultValue={new Date('2024-5-1')} onChange={(date) => {
    console.log(date.toLocaleDateString());
  }}/>
}

export default App


Here the Calendar component passes the defaultValue and onChange parameters.


The defaultValue will be used as the initial value of the value, and then the user will click on a different date to change the value, and then the onChange function will be called back.


In this case, the caller can only set the defaultValue initial value and cannot modify the value directly, so it is an uncontrolled mode.

 Try it;

 Then come back and write about the Controlled Mode version:

import { ChangeEvent, useEffect, useState } from "react"

interface CalendarProps{
  value: Date;
  onChange?: (date: Date) => void;
}
function Calendar(props: CalendarProps) {
  
  const {
    value,
    onChange
  } = props;

  function changeValue(date: Date) {
    onChange?.(date);
  } 

  return <div>
    {value.toLocaleDateString()}
    <div onClick={()=> {changeValue(new Date('2024-5-1'))}}>2023-5-1</div>
    <div onClick={()=> {changeValue(new Date('2024-5-2'))}}>2023-5-2</div>
    <div onClick={()=> {changeValue(new Date('2024-5-3'))}}>2023-5-3</div>
  </div>
}

function App() {
  const [value, setValue] = useState(new Date('2024-5-1'));

  return <Calendar value={value} onChange={(date) => {
    console.log(date.toLocaleDateString());
    setValue(date);
  }}/>
}

export default App


Use the value passed in by the props directly, and then call back the onChange function when you switch dates:

 The value of value is maintained at the caller.

 This is how controlled components are written:

 Can that support both controlled and uncontrolled modes?

 Yes, you can, that’s basically what component libraries do:

import { useEffect, useRef, useState } from "react"

interface CalendarProps{
  value?: Date;
  defaultValue?: Date;
  onChange?: (date: Date) => void;
}

function Calendar(props: CalendarProps) {
  
  const {
    value: propsValue,
    defaultValue,
    onChange
  } = props;

  const [value, setValue] = useState(() => {
    if (propsValue !== undefined) {
      return propsValue;
    } else {
      return defaultValue;
    }
  });

  const isFirstRender = useRef(true);

  useEffect(() => {
    if(propsValue === undefined && !isFirstRender.current) {
      setValue(propsValue);
    }
    isFirstRender.current = false;
  }, [propsValue]);

  const mergedValue = propsValue === undefined ? value : propsValue;

  function changeValue(date: Date) {
    if (propsValue === undefined) {
      setValue(date);
    }
    onChange?.(date);
  } 

  return <div>
    {mergedValue?.toLocaleDateString()}
    <div onClick={()=> {changeValue(new Date('2024-5-1'))}}>2023-5-1</div>
    <div onClick={()=> {changeValue(new Date('2024-5-2'))}}>2023-5-2</div>
    <div onClick={()=> {changeValue(new Date('2024-5-3'))}}>2023-5-3</div>
  </div>
}

function App() {
  return <Calendar defaultValue={new Date('2024-5-1')} onChange={(date) => {
    console.log(date.toLocaleDateString());
  }}/>
}

export default App


The parameter supports both value and defaultValue, and distinguishes between controlled and uncontrolled modes by determining whether the value is undefined or not.


In controlled mode, the initial value of useState is set to props.value, and then the rendering uses props.value.


If it’s uncontrolled mode, then the rendering uses the value of the internal state, and then setValue in changeValue.


When it is not the first rendering, but the value becomes undefined, that is, switching from controlled mode to uncontrolled mode, set the state to propsValue synchronously.

 In this way, the component supports both controlled and uncontrolled modes.

 Test it:

 Uncontrolled mode:

 Controlled mode:

 In fact, component libraries do it all.


For example, arco design’s useMergeValue hook:


The code is pretty much the same, it’s also usingState to set the value or defaultValue depending on whether the value is undefined or not.


But it adds another default value here, which defaultStateValue to use when there is no defaultValue.


Then the state used for rendering decides whether to use the value of the props or the value of the state by determining whether the value is undefind or not.


It also handles the case where value changes from something else to undefined:


The previous value is saved, and the internal state is modified to the value if it is judged to be undefined from a value other than props.value.

 Here the previous value is saved using useRef:


The feature of ref is that modifying the current attribute does not result in rendering.


We are judging non-first rendering, but props.value is changed to undefined, same effect.


Another example is the useMergedValue hook in the ant design toolkit rc-util:


It’s also useState which sets the value or defaultValue depending on whether the value is undefined or not.


Then it adds a default value, and when there is no defaultValue, it uses the defaultStateValue.


Rendering also determines whether the value is undefind to decide whether to use props.value or state’s value:


and also handles the fact that other values become undefined.

 Everyone does it, so let’s wrap a hook:

function useMergeState<T>(
  defaultStateValue: T,
  props?: {
    defaultValue?: T,
    value?: T
  }
): [T, React.Dispatch<React.SetStateAction<T>>,] {
  const { defaultValue, value: propsValue } = props || {};

  const isFirstRender = useRef(true);

  const [stateValue, setStateValue] = useState<T>(() => {
    if (propsValue !== undefined) {
      return propsValue!;
    } else if(defaultValue !== undefined){
      return defaultValue!;
    } else {
      return defaultStateValue;
    }
  });

  useEffect(() => {
    if(propsValue === undefined && !isFirstRender.current) {
      setStateValue(propsValue!);
    }

    isFirstRender.current = false;
  }, [propsValue]);

  const mergedValue = propsValue === undefined ? stateValue : propsValue;

  return [mergedValue, setStateValue]
}

 Use it:

interface CalendarProps{
  value?: Date;
  defaultValue?: Date;
  onChange?: (date: Date) => void;
}

function Calendar(props: CalendarProps) {
  const {
    value: propsValue,
    defaultValue,
    onChange
  } = props;

  const [mergedValue, setValue] = useMergeState(new Date(), {
    value: propsValue,
    defaultValue
  });

  function changeValue(date: Date) {
    if (propsValue === undefined) {
      setValue(date);
    }
    onChange?.(date);
  } 

  return <div>
    {mergedValue?.toLocaleDateString()}
    <div onClick={()=> {changeValue(new Date('2024-5-1'))}}>2023-5-1</div>
    <div onClick={()=> {changeValue(new Date('2024-5-2'))}}>2023-5-2</div>
    <div onClick={()=> {changeValue(new Date('2024-5-3'))}}>2023-5-3</div>
  </div>
}

 Try the effect:

 Uncontrolled mode:

 Controlled mode:


Then there’s this onChange part, which should be encapsulated as well:


Otherwise the user has to think about dealing with uncontrolled components when they use it.


I don’t see it encapsulated in arco design:


But ahooks’ useControllableValue encapsulates it:

 Let’s add that too:

import {  SetStateAction, useCallback, useEffect, useRef, useState } from "react"

function useMergeState<T>(
  defaultStateValue: T,
  props?: {
    defaultValue?: T,
    value?: T,
    onChange?: (value: T) => void;
  },
): [T, React.Dispatch<React.SetStateAction<T>>,] {
  const { defaultValue, value: propsValue, onChange } = props || {};

  const isFirstRender = useRef(true);

  const [stateValue, setStateValue] = useState<T>(() => {
    if (propsValue !== undefined) {
      return propsValue!;
    } else if(defaultValue !== undefined){
      return defaultValue!;
    } else {
      return defaultStateValue;
    }
  });

  useEffect(() => {
    if(propsValue === undefined && !isFirstRender.current) {
      setStateValue(propsValue!);
    }

    isFirstRender.current = false;
  }, [propsValue]);

  const mergedValue = propsValue === undefined ? stateValue : propsValue;

  function isFunction(value: unknown): value is Function {
    return typeof value === 'function';
  } 

  const setState = useCallback((value: SetStateAction<T>) => {
    let res = isFunction(value) ? value(stateValue) : value

    if (propsValue === undefined) {
      setStateValue(res);
    }
    onChange?.(res);
  }, [stateValue]);

  return [mergedValue, setState]
}

interface CalendarProps{
  value?: Date;
  defaultValue?: Date;
  onChange?: (date: Date) => void;
}

function Calendar(props: CalendarProps) {
  const {
    value: propsValue,
    defaultValue,
    onChange
  } = props;

  const [mergedValue, setValue] = useMergeState(new Date(), {
    value: propsValue,
    defaultValue,
    onChange
  });

  return <div>
    {mergedValue?.toLocaleDateString()}
    <div onClick={()=> {setValue(new Date('2024-5-1'))}}>2023-5-1</div>
    <div onClick={()=> {setValue(new Date('2024-5-2'))}}>2023-5-2</div>
    <div onClick={()=> {setValue(new Date('2024-5-3'))}}>2023-5-3</div>
  </div>
}

function App() {
  const [value, setValue] = useState(new Date('2024-5-1'));

  return <Calendar value={value} onChange={(date) => {
    console.log(date.toLocaleDateString());
    setValue(date);
  }}/>
  // return <Calendar defaultValue={new Date('2024-5-1')} onChange={(date) => {
  //   console.log(date.toLocaleDateString());
  // }}/>
}

export default App


Here it passes in onChange, and then when setState gets the new state value, or setStateValue if it’s uncontrolled mode, and then calls onChange.


Here we want to get the value of the previous value, considering the closure trap, so we use useCallback plus stateValue as a dependency to solve the problem.


When you use it, you don’t need to distinguish between controlled and uncontrolled, just setState:

 Try the effect:

 Uncontrolled mode:

 Controlled mode:


In this way, our component supports both controlled and uncontrolled modes.

By hbb

Leave a Reply

Your email address will not be published. Required fields are marked *