When I first started learning React, I had a hard time understanding its concepts. I still do, but as we all learn from our struggles, I did learn some very useful tips.
In this post I'm gonna share few tips which I've learned over the time when I was learning React.
Let's get started!
1. Event Handler With Closure
Before
type State = { name: string; number: string };
function App() {
const [state, setState] = useState<State>({
name: '',
number: '',
});
const handleNameChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const name = e.target.value.substring(0, 20);
setState((prevState) => ({
...prevState,
name,
}));
},
[]
);
const handleNumberChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const number =
e.target.value
.substring(0, 20)
.match(/[0-9]|-/g)
?.join('') || '';
setState((prevState) => ({
...prevState,
number,
}));
},
[]
);
return (
<div>
<span>Name</span>
<input type="text" onChange={handleNameChange} value={state.name} />
<span>Phone</span>
<input type="text" onChange={handleNumberChange} value={state.number} />
</div>
);
}
After
type State = { name: string; number: string };
type StateKey = keyof State;
type HandleStateChangeOption = { maxLength?: number; useTelFormat?: boolean };
function App() {
const [state, setState] = useState<State>({
name: '',
number: '',
});
const handleStateChange = useCallback(
(key: StateKey, options: HandleStateChangeOption) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
let { value } = e.target;
if (options.useTelFormat) {
value = value.match(/[0-9]|-/g)?.join('') || '';
}
if (options.maxLength !== undefined) {
value = value.substring(0, options.maxLength);
}
setState((prevState) => ({
...prevState,
[key]: value,
}));
},
[]
);
return (
<div>
<span>Name</span>
<input
type="text"
onChange={handleStateChange('name', { maxLength: 20 })}
value={state.name}
/>
<span>Phone</span>
<input
type="text"
onChange={handleStateChange('number', {
useTelFormat: true,
maxLength: 20,
})}
value={state.number}
/>
</div>
);
}
Now, you can control several states in a place and use the options.
2. Functional Components
The React functional components? No, Not. It means the components that do something without UI
.
Before
function App() {
const [enableScroll, setEnableScroll] = useState(true);
const [prevBodyOverflow, setPrevBodyOverflow] = useState<string>('auto');
const handleButtonClick = useCallback(() => {
if (enableScroll) {
setPrevBodyOverflow(document.body.style.overflow);
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = prevBodyOverflow;
}
setEnableScroll(!enableScroll);
}, [enableScroll, prevBodyOverflow]);
const buttonText = useMemo(
() => (enableScroll ? 'disable scroll' : 'enable scroll'),
[enableScroll]
);
return (
<div style={{ height: '200vh', backgroundColor: 'gray' }}>
<button onClick={handleButtonClick}>{buttonText}</button>
</div>
);
}
After
function DisableScroll() {
useEffect(() => {
const prevBodyOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = prevBodyOverflow;
};
}, []);
return null;
}
function App() {
const [enableScroll, setEnableScroll] = useState(true);
const handleButtonClick = useCallback(() => {
setEnableScroll(!enableScroll);
}, [enableScroll]);
const buttonText = useMemo(
() => (enableScroll ? 'disable scroll' : 'enable scroll'),
[enableScroll]
);
return (
<div style={{ height: '200vh', backgroundColor: 'gray' }}>
{!enableScroll && <DisableScroll />}
<button onClick={handleButtonClick}>{buttonText}</button>
</div>
);
}
Before
function App() {
const [count, setCount] = useState(60);
useEffect(() => {
const tm = setInterval(() => {
setCount((count) => {
if (count === 1) {
clearInterval(tm);
}
return count - 1;
});
}, 1000);
}, []);
return (
<div>
<span>{count}</span>
</div>
);
}
After
interface ICountProps {
onCount: (count: number) => void;
startNumber: number;
}
function Count({ onCount, startNumber }: ICountProps) {
const [count, setCount] = useState(startNumber);
useEffect(() => {
const tm = setInterval(() => {
setCount((count) => {
if (count === 1) {
clearInterval(tm);
}
return count - 1;
});
}, 1000);
}, []);
useEffect(() => {
onCount(count);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [count]);
return null;
}
function App() {
const [count, setCount] = useState(0);
return (
<div>
<Count startNumber={60} onCount={(count) => setCount(count)} />
<span>{count}</span>
</div>
);
}
I'm not sure it's a good example though. I believe you'll find the right case.
Replace your js code to components in a React way.
3. Separating map
from 'return' and use it with useMemo
before
type ItemType = { id: number; name: string; price: string };
type StateChangeHandler = (
state: keyof ItemType
) => (e: React.ChangeEvent<HTMLInputElement>) => void;
interface IItemProps extends ItemType {
onChange: StateChangeHandler;
onRemove: VoidFunction;
}
function Item({ id, name, price, onChange, onRemove }: IItemProps) {
return (
<div>
<span>ID: {id}</span>
<span>name</span>
<input type="text" value={name} onChange={onChange('name')} />
<span>price</span>
<input type="text" value={price} onChange={onChange('price')} />
<button onClick={onRemove}>REMOVE</button>
</div>
);
}
const newItem = (id: number): ItemType => ({ id, name: 'Item', price: '10' });
const initItems = (): ItemType[] => [newItem(1)];
function App() {
const [items, setItems] = useState<ItemType[]>(initItems());
const handleItemAdd = useCallback(() => {
setItems(items.concat(newItem(items[items.length - 1].id + 1)));
}, [items]);
const handleItemRemove = useCallback(
(index: number) => () => {
const newItems = [...items];
newItems.splice(index, 1);
setItems(newItems);
},
[items]
);
const handleStateChange = useCallback(
(index: number) =>
(state: keyof ItemType) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const newItems = [...items];
switch (state) {
case 'id':
newItems[index][state] = parseInt(e.target.value, 10);
break;
default:
newItems[index][state] = e.target.value;
}
setItems(newItems);
},
[items]
);
return (
<div>
<button onClick={handleItemAdd}>ADD</button>
<br />
{items.map((item, itemIdx) => (
<Item
key={itemIdx}
{...item}
onChange={handleStateChange(itemIdx)}
onRemove={handleItemRemove(itemIdx)}
/>
))}
</div>
);
}
After
...
const itemComponents = useMemo(() => {
return items.map((item, itemIdx) => (
<Item
key={itemIdx}
{...item}
onChange={handleStateChange(itemIdx)}
onRemove={handleItemRemove(itemIdx)}
/>
));
}, [handleItemRemove, handleStateChange, items]);
return (
<div>
<button onClick={handleItemAdd}>ADD</button>
<br />
{itemComponents}
</div>
);
It looks focus more First thing
.
But the point is keeping your return
clean.
4. Conditional Rendering with Compound Components
Before
function App() {
const [number, setNumber] = useState('');
const handleNumberChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) =>
setNumber(e.target.value.match(/[0-9]/g)?.join('') || ''),
[]
);
const realNum = Number(number);
return (
<div>
<input type="text" value={number} onChange={handleNumberChange} />
<br />
{realNum < 10 && <div>number is less than 10.</div>}
{realNum > 10 && <div>number is greater than 10.</div>}
{realNum < 5 && <div>number is less than 5.</div>}
{realNum > 3 && <div>number is greater than 3.</div>}
</div>
);
}
After
interface IContext {
contextNum: number;
}
interface INumComponentProps {
num: number;
children?: React.ReactNode;
}
const CondtionalRenderingContext = React.createContext<IContext>({
contextNum: 0,
});
const useContextNum = () => React.useContext(CondtionalRenderingContext);
function CondtionalRendering({ num, children }: INumComponentProps) {
return (
<CondtionalRenderingContext.Provider value={{ contextNum: num }}>
{children}
</CondtionalRenderingContext.Provider>
);
}
function Over({ num, children }: INumComponentProps) {
const { contextNum } = useContextNum();
if (contextNum <= num) return null;
return <>{children}</>;
}
function Under({ num, children }: INumComponentProps) {
const { contextNum } = useContextNum();
if (contextNum >= num) return null;
return <>{children}</>;
}
function App() {
const [number, setNumber] = useState('');
const handleNumberChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) =>
setNumber(e.target.value.match(/[0-9]/g)?.join('') || ''),
[]
);
const realNum = Number(number);
return (
<div>
<input type="text" value={number} onChange={handleNumberChange} />
<br />
<CondtionalRendering num={realNum}>
<Under num={10}>
<div>number is less than 10.</div>
</Under>
<Over num={10}>
<div>number is greater than 10.</div>
</Over>
<Under num={5}>
<div>number is less than 5.</div>
</Under>
<Over num={3}>
<div>number is greater than 3.</div>
</Over>
</CondtionalRendering>
</div>
);
}
It's not always a better way. There are more components, it can be not good in performance. Consider it whether the benefits are better than the shortcoming or not in your code. (Like readability)
5. Custom Dialogs with context
Before
interface IDialog {
message: string;
onConfirm?: VoidFunction;
}
function Dialog({ message, onConfirm }: IDialog) {
return (
<ModalOverlay>
<ModalContainer>
<p>{message}</p>
<button onClick={onConfirm}>OK</button>
</ModalContainer>
</ModalOverlay>
);
}
function App() {
const [text, setText] = useState('');
const [dialog, setDialog] = useState<IDialog | undefined>(undefined);
const handleDialogOpen = useCallback(() => {
setDialog({
message: text,
onConfirm: () => {
setText('');
setDialog(undefined);
},
});
}, [text]);
return (
<div>
{dialog && <Dialog {...dialog} />}
<input
type="text"
onChange={(e) => setText(e.target.value)}
value={text}
/>
<button onClick={handleDialogOpen}>Click Me!</button>
</div>
);
}
const ModalOverlay = styled.div`
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
`;
const ModalContainer = styled.div`
min-width: 300px;
padding: 24px;
background-color: white;
box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
border-radius: 12px;
> button {
width: 100%;
}
`;
After
interface IDialog {
message: string;
onConfirm?: VoidFunction;
}
interface IDialogContext {
showDialog: (paramaeters: IDialog) => void;
}
const DialogContext = React.createContext<IDialogContext>(undefined!);
const useDialog = () => React.useContext(DialogContext);
function DialogContextProvider({ children }: { children?: React.ReactNode }) {
const [dialog, setDialog] = useState<IDialog | undefined>(undefined);
const showDialog = useCallback(({ message, onConfirm }: IDialog) => {
setDialog({
message,
onConfirm: () => {
onConfirm && onConfirm();
setDialog(undefined);
},
});
}, []);
return (
<DialogContext.Provider value={{ showDialog }}>
{dialog && <Dialog {...dialog} />}
{children}
</DialogContext.Provider>
);
}
function Dialog({ message, onConfirm }: IDialog) {
return (
<ModalOverlay>
<ModalContainer>
<p>{message}</p>
<button onClick={onConfirm}>OK</button>
</ModalContainer>
</ModalOverlay>
);
}
function Page() {
const [text, setText] = useState('');
const { showDialog } = useDialog();
const handleDialogOpen = useCallback(() => {
showDialog({
message: text,
onConfirm: () => setText(''),
});
}, [text]);
return (
<div>
<input
type="text"
onChange={(e) => setText(e.target.value)}
value={text}
/>
<button onClick={handleDialogOpen}>Click Me!</button>
</div>
);
}
function App() {
return (
<DialogContextProvider>
<Page />
</DialogContextProvider>
);
}
const ModalOverlay = styled.div`
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
justify-content: center;
align-items: center;
`;
const ModalContainer = styled.div`
min-width: 300px;
padding: 24px;
background-color: white;
box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
border-radius: 12px;
> button {
width: 100%;
}
`;
I made the page component for avoiding confusing where you should put the Provider. You don't have to take care all of the dialog code. Just get the show method from useDialog
anywhere then use it.
showDialog({ message: 'hi!' });
Also, you can make other dialogs and modals like this way.
Conclusion
Here are 5 React useful tips. I hope the post helps someone. If you have better examples with these tips or if you have your useful tips, please let me know in the comment below.
Thank you for reading this and happy React
!