Reacting to input with state
- With React, you won’t modify the UI from code directly.
- Describe different state the component can be in and do the logic based on that
imperative vs declerative”
“imperative : we have to write the exact instructions to manipulate the UI depending on what just happened (eg: explaining turn by turn direction to a driver)”
“declerative : declare what you want to show, and the system figures out how to update the UI (eg: telling driver where to go).
Thinking in react
- Identify your component’s different visual states
- prepare a [story book]()
- Determine what triggers those state changes
- Triggers might be system or human generated.
- Represent the state in memory using useState
- use minimum states needed.
- Remove any non-essential state variables
- same as above, reduce relationship between states so that we can use less state varible and use existing one (eg:
isSubmitted
and!isSubmitted
). - Reducers let you unify multiple state variables into a single object and consolidate all the related logic!
- same as above, reduce relationship between states so that we can use less state varible and use existing one (eg:
- Connect the event handlers to set the state.
Principles for structuring state
- Group related state.
- Combine multiple state to one if needed.
const [position, setPosition] = useState({ x: 0, y: 0 });
- Combine multiple state to one if needed.
- Avoid contradictions in state.
- if there is a possibility of conflicted (impossible or confusing) states, refactor. insted of
const [isSending, setIsSending] = useState(false); const [isSent, setIsSent] = useState(false);
useconst [status, setStatus] = useState('typing');
- if there is a possibility of conflicted (impossible or confusing) states, refactor. insted of
- Avoid redundant state.
- If one variable can be infered from existing state, dont create a new state.
const [firstName, setFirstName] = useState('');const [lastName, setLastName] = useState('');const [fullName, setFullName] = useState('');
fullname is reduntant state hence avoid that.
- If one variable can be infered from existing state, dont create a new state.
- Avoid duplication in state.
- When the same data is duplicated between multiple state variables, or within nested objects, it is difficult to keep them in sync. Reduce duplication when you can.
- Avoid deeply nested state.
- Deeply hierarchical state is not very convenient to update. When possible, prefer to structure state in a flat way.
- example : use
childIds: [2, 10, 19, 26, 34]
insted of adding each object in here. - Dont just do soft remove . prefer hard remove.
Don’t mirror props in state like
function Message({ messageColor }) { const [color, setColor] = useState(messageColor);
. ifmessageColor
is changed by parent , child component (Message in here) wont be aware of that . we can consider using messageColor as an initial value. we can directly usemessageColor
in our child insted on mirroring.
Sharing state between components
- Moving the state we need manage for childern to the immediate parent is a way to achive this aka lifting state up.
- controlled and uncontrolled component: a component which can be managed by parent is called controlled.
- For each state we have to identify its owner, it might change as the app evolves, or in another term we have to make it a single source of truth.
preserving and resetting state
- In React, each component (even if we have same component twice in a render tree) on the screen has fully isolated state.
- If one component is removed from the tree, its and its childern’s states are also destroyed.
- If we replace one component with same componet in the react UI tree, its state is preserved.
- Don’t nest component function definitions , when a component is rendereed its nested component (say A) definition is rerun creating it new ‘instance’, which will destroy the state.
export default function MyComponent() {
const [counter, setCounter] = useState(0);
function MyTextField() { <-- nested component fucntion
const [text, setText] = useState('');
return (
<input
value={text}
onChange={e => setText(e.target.value)}
/>
);
}
return (
<>
<MyTextField /> <-- recreated in every rerender
<button onClick={() => {
setCounter(counter + 1)
}}>Clicked {counter} times</button>
</>
);
}
- If we want to change state even if we want to keep the component in position
- render the component in another place
- give each component different key
- Specifying a key tells React to use the key itself as part of the position, instead of their order within the parent.
Preserving state for removed components
- Dont remove component.(use css to hide).
- save message in parent (lift the state up).
- use local storage.
State Reducers
- move that state logic into a single function outside your component, called a “reducer”.
- specify “what the user just did” by dispatching “actions” from your event handlers.
function handleAddTask(text) {
dispatch(
// action object
{
type: 'added',
id: nextId++,
text: text,
}
);
}
- write a reducer(?) function (we can use if/else or switch(preferrd as a convention))
Reducer take the state so far and the action, and return the next state.
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
}
}
- use Reducer
import { useReducer } from 'react';
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer vs useState
Criteria | useState | useReducer |
---|---|---|
Code Size | Less code upfront. | Requires writing a reducer function and dispatch actions. Can reduce code if many event handlers modify state similarly. |
Readability | Easy to read for simple state updates. Can bloat component code for complex updates. | Separates update logic from event handlers, improving readability for complex updates. |
Debugging | Difficult to trace where state was set incorrectly. | Can log every state update and action in the reducer, making it easier to debug. Requires stepping through more code. |
Testing | State updates are tied to the component. | Reducer is a pure function, allowing for isolated testing. Useful for complex state update logic. |
Preference | Some prefer useState for its simplicity. | Others prefer useReducer for its structure. Both are interchangeable based on preference. |
Note
- Reducers must be pure (similar to state updater fucntion)
- They should not send requests, schedule timeouts, or perform any side effects.
- They should update objects and arrays without mutations.
- Each action describes a single user interaction
Reducers using Immer
- Immer provides you with a special draft object which is safe to mutate.
React “Context”
-
Context lets the parent component make some information available to any component in the tree below it.
-
We can avoid props drilling (passing props to nodes by skipping some parent nodes).
-
- Create Context
import { createContext } from 'react'; export const LevelContext = createContext(1);
- Use Context
const level = useContext(LevelContext);
- Provide Context (“if any component inside this
<Section>
asks for LevelContext, give them this level). it will use nearest<LevelContext.Provider>
in the UI tree above it
export default function Section({ level, children }) { return ( <section className="section"> <LevelContext.Provider value={level}> {children} </LevelContext.Provider> </section> );
-
We can use and provide a context in the same component.
-
Context will passes through intermediate components.
-
Overiding one context provider with another provider for a nodes parent is possible. Alternate approach for context
-
Prop drilling
-
Extract components and pass JSX as
children
to them.
Use cases for context
- Theming (light and dark mode etc.)
- Current account (auth token / user profile etc)
- Routing
- Managing state
Reducer and Context
- We can use reducer in a context to manage complex screens.
- Put all the state and dispatch function to a context and use it everywhere down the tree.
- Steps to implement are exactly same as above. see example
== TaskContext.js ==
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
// task reducer implementation