Skip to main content

React Gotchas

Typing event handlers:

interface MyComponentProps {
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}

const MyComponent = ({ onClick }: MyComponentProps) => {
return <button onClick={onClick}>Click Me</button>;
};

Handling optional props:

interface MyComponentProps {
requiredProp: string;
optionalProp?: string;
}

const MyComponent = ({ requiredProp, optionalProp = "default" }: MyComponentProps) => {
return (
<div>
<p>{requiredProp}</p>
<p>{optionalProp}</p>
</div>
);
};

Using generics

interface MyComponentProps<T> {
items: T[];
render: (item: T) => React.ReactNode;
}

const MyComponent = <T extends {}>({ items, render }: MyComponentProps<T>) => {
return <div>{items.map(render)}</div>;
};

Using the correct types for useState and useEffect:

interface MyComponentProps {
initialCount: number;
}

const MyComponent = ({ initialCount }: MyComponentProps) => {
const [count, setCount] = React.useState<number>(initialCount);

React.useEffect(() => {
console.log("Count changed:", count);
}, [count]);

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};

Using the correct syntax for Reducers

interface MyState {
count: number;
}

interface IncrementAction {
type: "INCREMENT";
payload: number;
}

interface DecrementAction {
type: "DECREMENT";
payload: number;
}

type MyAction = IncrementAction | DecrementAction;

const reducer = (state: MyState, action: MyAction): MyState => {
switch (action.type) {
case "INCREMENT":
return { count: state.count + action.payload };
case "DECREMENT":
return { count: state.count - action.payload };
default:
return state;
}
};

Understanding how to use defaultProps (old way with classes):

interface MyComponentProps {
requiredProp: string;
optionalProp?: string;
}

const MyComponent = ({ requiredProp, optionalProp = "default" }: MyComponentProps) => {
return (
<div>
<p>{requiredProp}</p>
<p>{optionalProp}</p>
</div>
);
};

MyComponent.defaultProps = {
optionalProp: "default",
};

Handling nested props:

interface MyComponentProps {
user: {
name: string;
age: number;
};
}

const MyComponent = ({ user }: MyComponentProps) => {
return (
<div>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
</div>
);
};

Avoiding the any type:

// Avoid using any
const MyComponent = (props: any) => {
return <div>{props.someProp

Understanding how to use useRef:

interface MyComponentProps {
initialText: string;
}

const MyComponent = ({ initialText }: MyComponentProps) => {
const inputRef = React.useRef<HTMLInputElement>(null);

React.useEffect(() => {
if (inputRef.current) {
inputRef.current.value = initialText;
}
}, [initialText]);

return <input type="text" ref={inputRef} />;
};

Typing custom hooks:

interface UseCounterResult {
count: number;
increment: () => void;
}

const useCounter = (initialCount: number): UseCounterResult => {
const [count, setCount] = React.useState<number>(initialCount);

const increment = () => {
setCount(count + 1);
};

return { count, increment };
};

const MyComponent = () => {
const { count, increment } = useCounter(0);

return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
};

Handling generic props with React.ReactNode

interface MyComponentProps<T> {
children: React.ReactNode;
render: (item: T) => React.ReactNode;
}

const MyComponent = <T extends {}>({ children, render }: MyComponentProps<T>) => {
return (
<div>
{children}
{render({})}
</div>
);
};

Using the correct types for useContext:

interface MyContextType {
count: number;
increment: () => void;
}

const MyContext = React.createContext<MyContextType | undefined>(undefined);

const MyComponent = () => {
const context = React.useContext(MyContext);

return (
<div>
<p>Count: {context?.count}</p>
<button onClick={context?.increment}>Increment</button>
</div>
);
};

Avoiding infinite loops with useEffect:

note: you should not use an object or array as a dependency in useEffect, only primitives

interface MyComponentProps {
count: number;
}

const MyComponent = ({ count }: MyComponentProps) => {
const [value, setValue] = React.useState<number>(0);

React.useEffect(() => {
if (value !== count) {
setValue(count);
}
}, [count, value]);

return <div>Value: {value}</div>;
};

Understanding how to use useCallback:

interface MyComponentProps {
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}

const MyComponent = React.memo(({ onClick }: MyComponentProps) => {
const memoizedOnClick = React.useCallback(onClick, []);

return <button onClick={memoizedOnClick}>Click Me</button>;
});

Using the correct syntax for React.useMemo:

interface MyComponentProps {
count: number;
}

const MyComponent = ({ count }: MyComponentProps) => {
const memoizedValue = React.useMemo(() => count * 2, [count]);

return <div>Value: {memoizedValue}</div>;
};

Typing Higher Order Components (HOCs):

interface WithLoggingProps {
logMessage: string;
}

function withLogging<T extends WithLoggingProps>(
WrappedComponent: React.ComponentType<T>
) {
return (props: T) => {
React.useEffect(() => {
console.log(props.logMessage);
}, [props.logMessage]);

return <WrappedComponent {...props} />;
};
}

interface MyComponentProps {
name: string;
}

const MyComponent = withLogging(({ name }: MyComponentProps & WithLoggingProps) => {
return <div>Hello, {name}!</div>;
});

Handling optional chaining inside JSX

interface MyComponentProps {
user: {
name: string;
address?: {
street: string;
city: string;
};
};
}

const MyComponent = ({ user }: MyComponentProps) => {
return (
<div>
<p>Name: {user.name}</p>
<p>
Address: {user.address?.street}, {user.address?.city}
</p>
</div>
);
};

Using the correct types for useState with state:

interface MyComponentProps {
initialUser: {
name: string;
age: number;
};
}

const MyComponent = ({ initialUser }: MyComponentProps) => {
const [user, setUser] = React.useState<{ name: string; age: number }>(initialUser);

const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setUser((prevUser) => ({ ...prevUser, name: event.target.value }));
};

const handleAgeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setUser((prevUser) => ({ ...prevUser, age: parseInt(event.target.value) }));
};

return (
<div>
<input type="text" value={user.name} onChange={handleNameChange} />
<input type="number" value={user.age} onChange={handleAgeChange} />
</div>
);
};

Typing dynamically rendered components

interface MyComponentProps {
component: React.ComponentType<any>;
}

const MyComponent = ({ component: Component }: MyComponentProps) => {
return <Component />;
};

useEffect: Understanding how to use the dependencies array:

The dependencies array is used to specify which values the effect depends on. It's important to only include the values that the effect actually uses, as including unnecessary values can lead to performance issues.

interface MyComponentProps {
count: number;
}

const MyComponent = ({ count }: MyComponentProps) => {
React.useEffect(() => {
console.log("Count changed:", count);
}, [count]);

return <div>Count: {count}</div>;
};

useEffect: Handling cleanup functions

interface MyComponentProps {
count: number;
}

const MyComponent = ({ count }: MyComponentProps) => {
React.useEffect(() => {
const intervalId = setInterval(() => {
console.log("Count:", count);
}, 1000);

return () => {
clearInterval(intervalId);
};
}, [count]);

return <div>Count: {count}</div>;
};

useEffect: Using multiple effects:

Multiple effects can be used in a single component, but it's important to properly handle each effect and its dependencies.

interface MyComponentProps {
count: number;
}

const MyComponent = ({ count }: MyComponentProps) => {
React.useEffect(() => {
console.log("Count changed:", count);
}, [count]);

React.useEffect(() => {
console.log("Component mounted");
return () => {
console.log("Component unmounted");
};
}, []);

return <div>Count: {count}</div>;
};

useEffect: Handling async effects:

Async effects can be used to perform asynchronous operations, but it's important to properly handle any errors that may occur.

interface MyComponentProps {
userId: number;
}

const MyComponent = ({ userId }: MyComponentProps) => {
const [user, setUser] = React.useState<User | null>(null);

React.useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch(`https://example.com/users/${userId}`);
const data = await response.json();
setUser(data);
} catch (error) {
console.error(error);
}
};

fetchUser();
}, [userId]);

return <div>User: {user?.name}</div>;
};

Understanding how to update complex state:

When updating complex state (such as an object or an array), it's important to use the spread operator or a library like Immer to ensure that the state is updated immutably.

interface MyComponentProps {
initialUser: {
name: string;
age: number;
};
}

const MyComponent = ({ initialUser }: MyComponentProps) => {
const [user, setUser] = React.useState(initialUser);

const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, name: event.target.value });
};

const handleAgeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setUser({ ...user, age: parseInt(event.target.value) });
};

return (
<div>
<input type="text" value={user.name} onChange={handleNameChange} />
<input type="number" value={user.age} onChange={handleAgeChange} />
</div>
);
};

useState: Avoiding stale state:

When updating state based on previous state, it's important to use the function form of setState to avoid stale state issues.

interface MyComponentProps {
initialCount: number;
}

const MyComponent = ({ initialCount }: MyComponentProps) => {
const [count, setCount] = React.useState(initialCount);

const increment = () => {
setCount((prevCount) => prevCount + 1);
};

return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
};

useState: Understanding the difference between primitives and objects:

const [count, setCount] = React.useState(0); // primitive

const [user, setUser] = React.useState({ name: "", age: 0 }); // object

const [todos, setTodos] = React.useState<string[]>([]); // array

useState: Handling async state updates:

When updating state asynchronously (such as when fetching data from an API), it's important to properly handle any errors that may occur.

interface MyComponentProps {
userId: number;
}

const MyComponent = ({ userId }: MyComponentProps) => {
const [user, setUser] = React.useState<User | null>(null);

React.useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch(`https://example.com/users/${userId}`);
const data = await response.json();
setUser(data);
} catch (error) {
console.error(error);
}
};

fetchUser();
}, [userId]);

return <div>User: {user?.name}</div>;
};

Avoiding unnecessary re-renders:

interface MyComponentProps {
count: number;
}

const MyComponent = ({ count }: MyComponentProps) => {
const [value, setValue] = React.useState(0);

React.useEffect(() => {
setValue(count * 2);
}, [count]);

return <div>Value: {value}</div>;
};

const MemoizedComponent = React.memo(MyComponent, (prevProps, nextProps) => {
return prevProps.count === nextProps.count;