mithi / react-philosophies
- пятница, 1 октября 2021 г. в 00:27:04
🧘 Things I think about when I write React code 🧘
If react-philosophies
helped you in some way, consider buying me a few cups of coffee React
"stuff"!
While writing, I realized that it was actually difficult for me to separate my thoughts into the design
, performance
, and testing
. A lot of designs intended for maintainability also make your application faster and easier to test. Apologies if the discussion appears to be cluttered at times.
If there's something that you think should be part of my reading list, or if you have great ideas that you think I should include here, don't hesitate to submit a PR or an issue; I'll check it out. Any contributions to improve react-philosophies
whether big or small are always welcome and appreciated.
Special thanks to the r/reactjs
community for giving very valuable suggestions that helped significantly improve the quality of this document.
goldbergyoni/nodebestpractices
, recommended by rstacruzreact-philosophies
is:
React
code.React
specifically A lot of these things may feel like very basic and common-sense. But surprisingly, I've worked with large complex applications where these things are not taken into consideration. The examples I present here are based on code I have actually seen in production.
react-philosophies
is inspired by various places I've stumbled upon at different points of my coding journey.
Most notably:
ESLint
. Enable the rule-of-hooks
and exhaustive-deps
rule to catch React
-specific errors.Typescript
will make your life so much easier.NextJS
is an awesome framework.exhaustive-deps
warnings / errors on your useMemo
's, useCallback
's and useEffect
's. You can try "The latest ref pattern" to keep your callbacks always up-to-date without unnecessary rerenders.map
to display components.tree-shaking
is your friend!"The best code is no code at all. Every new line of code you willingly bring into the world is code that has to be debugged, code that has to be read and understood, code that has to be supported." - Jeff Atwood
"One of my most productive days was throwing away 1000 lines of code." - Eric S. Raymond
"I hate code and I want as little of it as possible in our product" - Jack Diederich
"If I had more time, I would have written a shorter letter" - Blaise Pascal, Mark Twain, among others..
See also: Write Less Code - Richard Hariss (Svelte), Washing Code: Code is evil - Artem Sapegin
TL;DR
React
Needless to say, the more you add dependencies, the more code you ship to the browser. Ask yourself, are you actually using the features which make a particular library great?
Redux
? It's possible. But keep in mind that React is already a state management library.Apollo client
? Apollo client has many awesome features, like manual normalization. However, it will significantly increase your bundle size. If your application only makes use of features that are not unique to Apollo client , consider using a smaller library such as react-query
or SWR
(or none at all).Axios
? Axios is a great library with features that are not easily replicable with native fetch
. But if the only reason for using Axios is that it has a better looking API, then consider just using a wrapper on top of fetch (such as redaxios
or your own). Determine whether or not your application is actually using Axios's best features.Lodash
/underscoreJS
? you-dont-need/You-Dont-Need-Lodash-UnderscoreMomentJS
? you-dont-need/You-Dont-Need-MomentjsContext
for theming (light
/dark
mode), consider using css variables
instead.Javascript
. CSS is powerful. you-dont-need/You-Dont-Need-JavaScriptReact
React
is just Javascript
and Javascript
is just code
map
, filter
, find
, findIndex
, some
, etc)"What could happen with my software in the future? Oh yeah, maybe this and that. Let’s implement all these things since we are working on this part anyway. That way it’s future-proof."
You Aren't Gonna Need It! Always implement things when you actually need them, never when you just foresee that you may need them.
See also: Martin Fowler: YAGNI, C2 Wiki: You Arent Gonna Need It!, C2: YAGNI (original), Jack Diederich: Stop Writing Classes
A window gets broken at an apartment building, but no one fixes it. It's left broken. Then something else gets broken. Maybe it's an accident, maybe not, but it isn't fixed either. Graffiti starts to appear. More and more damage accumulates. Very quickly you get an exponential ramp. The whole building decays. Tenants move out. Crime moves in. And you've lost the game. It's all over. You don't want to let technical debt get out of hand. You want to stop the small problems before they grow into big problems. - Don't Live with Broken Windows: A Conversation with Andy Hunt and Dave Thomas, Part I
If you recognize that something is wrong, fix it right there and there. But if it's not that easy to fix or you don't have time to fix it at that moment, add a comment FIXME
or TODO
and add a short explanation of the problem you've identified. Make sure everybody knows it is broken. It shows other people that you care and that they should also do the same when they encounter those kinds of things.
TIP: Remember that you may not need to put your state
as a dependency because you can pass a callback function instead.
You don't need to put setState
(from useState
) and dispatch
(from useReducer
) in your dependency array for hooks like useEffect
and useCallback
. ESLint will NOT complain because React guarantees their stability.
❌ Not-so-good
const decrement = useCallback(() => setCount(count - 1), [setCount, count])
const decrement = useCallback(() => setCount(count - 1), [count])
✅ BETTER
const decrement = useCallback(() => setCount(count => (count - 1)), [])
TIP: If your useMemo
or useCallback
doesn't have a dependency, you might be using it wrong.
❌ Not-so-good
const MyComponent = () => {
const functionToCall = useCallback(x: string => `Hello ${x}! I am actually doing more than this`,[])
const iAmAConstant = useMemo(() => { return {x: 5, y: 2} }, [])
/* I will use functionToCall and iAmAConstant */
}
✅ BETTER
const I_AM_A_CONSTANT = { x: 5, y: 2 }
const functionToCall = (x: string => `Hello ${x}! I am actually doing more than this`)
const MyComponent = () => {
/* I will use functionToCall and I_AM_A_CONSTANT */
}
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." - Martin Fowler
"The ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. So if you want to go fast, if you want to get done quickly, if you want your code to be easy to write, make it easy to read." ― Robert C. Martin
TL;DR
Context
is not the solution for every state sharing problemuseEffect
s to smaller independent ones (KCD: Myths about useEffect)logical
and presentational
components (but not necessarily, use your best judgement)useCallback
, useMemo
, and useEffect
useCallback
, useMemo
, and useEffect
useState
s, consider using useReducer
if some values of your state rely on other values of your state and previous stateWhen you have redundant states, some states may fall out of sync; you may forget to update them given a complex sequence of interactions.
Aside from avoiding synchronization bugs
, you'd notice that it's also easier to reason about and require less code.
See also: KCD: Don't Sync State. Derive It!, Tic-Tac-Toe
Note: For the following two examples, assume that the number of items to be fetched in always less than 100 (meaning you don't need to worry about optimization).
If you're working with really large numbers of items, you can memoize the some computations with useMemo
.
You are tasked to display properties of each right triangle from a list
A list of two numbers {a: number, b: number}[]
should be fetched from an API. The two numbers represent the two shorter sides of a right triangle.
const TriangleInfo = () => {
const [triangleInfo, setTriangleInfo] = useTriangles<{a: number, b: number}>([])
const [hypotenuses, setHypotenuses] = useState<number[]>([])
const [perimeters, setPerimeters] = useState<number[]>([])
const [areas, setAreas] = useState<number[]>([])
useEffect(() => {
fetchTriangles().then(r => {
setTriangleInfo(r)
setHypotenuses(r.map(t => computeHypotenuse(t.a, t.b))
setArea(r.map(t => computeArea(t.a, t.b))
})
}, [])
useEffect(() => {
setHypotenuses(triangleInfo.map(t => computeHypotenuse(t.a, t.b))
setArea(triangleInfo.map(t => computeArea(t.a, t.b))
}, [triangleInfo])
useEffect(() => {
const p = triangleInfo((t, i) => {
return computePerimeter(t.a, t.b, hypotenuse[i])
})
}, [triangleInfo, hypotenuses])
/*** show info here ****/
}
const TriangleInfo = () => {
const [triangleInfo, setTriangleInfo] = useTriangles<{
a: number;
b: number;
}>([]);
useEffect(() => {
fetchTriangles().then((r) => setTriangleInfo(r));
}, []);
const areas = triangleInfo.map((t) => computeArea(t.a, t.b));
const hypotenuses = triangleInfo.map((t) => computeHypotenuse(t.a, t.b));
const perimeters = triangleInfo.map((t, i) =>
computePerimeters(t.a, t.b, hypotenuses[i])
);
/*** show info here ****/
};
Suppose you are assigned to design a component which:
x
or y
(ascending order)maxDistance
(increase + 10)maxDistance
from the origin (0, 0)
type SortBy = 'x' | 'y'
const toggle = (current: SortBy): SortBy => current === 'x' ? : 'y' : 'x'
const Points = () => {
const [points, setPoints] = useState<{x: number, y: number}[]>([])
const [filteredPoints, setFilteredPoints] = useState<{x: number, y: number}[]>([])
const [sortedPoints, setSortedPoints] = useState<{x: number, y: number}[]>([])
const [maxDistance, setMaxDistance] = useState<number>(Infinity)
const [sortBy, setSortBy] = useState<SortBy>('x')
useEffect(() => {
fetchPoints().then(r => setPoints(r))
}, [])
useEffect(() => {
setSortedPoints(sortPoints(points, sortBy))
}, [sortBy, points])
useEffect(() => {
setFilteredPoints(sortedPoints.filter(p => getDistance(p.x, p.y) < maxDistance))
}, [sortedPoints, maxDistance])
const otherSortBy = toggle(sortBy)
return (
<>
<button onClick={() => setSortBy(otherSortBy)}>Sort by: {otherSortBy}<button>
<button onClick={() => setMaxDistance(maxDistance + 5)}>Increase max distance<button>
Showing only points that are less than {maxDistance} units away from origin (0, 0)
Currently sorted by: '{sortBy}' (ascending)
<ol>{filteredPoints.map(p => <li key={`${p.x}|{p.y}`}>({p.x}, {p.y})</li>}
</>
)
}
// NOTE:
// You can also use useReducer instead
type SortBy = 'x' | 'y'
const toggle = (current: SortBy): SortBy => current === 'x' ? : 'y' : 'x'
const Points = () => {
const [points, setPoints] = useState<{x: number, y: number}[]>([])
const [maxDistance, setMaxDistance] = useState<number>(Infinity)
const [sortBy, setSortBy] = useState<SortBy>('x')
useEffect(() => {
fetchPoints().then(r => setPoints(r))
}, [])
const otherSortBy = toggle(sortBy)
return (
<>
<button onClick={() => setSortBy(otherSortBy)}>Sort by: {otherSortBy} <button>
<button onClick={() => setMaxDistance(maxDistance + 10)}>Increase max distance<button>
Showing only points that are less than {maxDistance} units away from origin (0, 0)
Currently sorted by: '{sortBy}' (ascending)
<ol>{
sortPoints(
points.filter(p => getDistance(p.x, p.y) < maxDistance),
sortBy
).map(p => <li key={`${p.x}|{p.y}`}>({p.x}, {p.y})</li>
}
</>
)
}
You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. - Joe Armstrong, creator of Erlang
To avoid falling into this trap, it's a good idea to pass mostly primitives (boolean
, string
, number
, etc) types as props. (Passing primitives is also a good idea if you want to use React.memo
for optimization)
A component should just know enough to do its job and nothing more. As much as possible, components should be able to collaborate with others without knowing what they are and what they do.
When we do this, the components will be loosely coupled. Loose coupling means that the degree of dependency between two components is low. Loose coupling makes it easier to change, replace, or remove components without affecting other components. See also: stackoverflow:2832017
Create a UserCard
component that displays two components: Summary
and SeeMore
. The SeeMore
component includes presenting the age
and bio
of the user
. Include a button to toggle between showing and hiding the age
and bio
of the user
.
The Summary
component displays the profile picture of the user and also his title
, firstName
and lastName
(e.g. Mr. Vincenzo Cassano
). Clicking the user's name should take you to the user's personal site. The Summary
component may also have other functionalities. (Say for example, randomly changing the font, size of the image, and background color whenever this component is clicked.. for brevity let's call this "the random styling feature")
The UserCard
calls the hook useUser
that returns an object with the type below.
type User = {
firstName: string;
lastName: string;
title: string;
imgUrl: string;
webUrl: string;
age: number;
bio: string;
/****** 100 or more fields ******/
};
const Summary = ({ user } : { user: User }) => {
/*** include "the random styling feature" ***/
return (
<>
<img src={user.imgUrl} />
<a href={user.webUrl}>{user.title}. {user.firstName} {user.lastName}</a>
</>
)
}
const SeeMore = ({ user }: { user: User }) => {
const [seeMore, setSeeMore] = useState<boolean>(false)
return (
<>
<button onClick={() => setSeeMore(!seeMore)}>See more</button>
{seeMore && <>AGE: {user.age} | BIO: {user.bio}</>}
</>
)
}
const UserCard = () => {
const user = useUser()
return <><Summary user={user} /><SeeMore user={user} /></>
}
const Summary = ({ imgUrl, webUrl, displayName }: { imgUrl: string, webUrl: string, displayName: string }) => {
/*** include "the random styling feature" ***/
return (
<>
<img src={imgUrl} />
<a href={webUrl}>{displayName}</a>
</>
)
}
const SeeMore = ({ componentToShow }: {componentToShow: ReactNode }) => {
const [seeMore, setSeeMore] = useState<boolean>(false)
return (
<>
<button onClick={() => setSeeMore(!seeMore)}>See more</button>
{seeMore && <>{componentToShow}</>}
</>
)
}
const UserCard = () => {
const { title, firstName, lastName, webUrl, imgUrl, age, bio } = useUser()
return (
<>
<Summary displayName={`${title}. ${firstName} ${lastName}`} {...{imgUrl, webUrl}} />
<SeeMore componentToShow={<>AGE: {age} | BIO: {bio}</>} />
</>
)
}
(The paragraphs below is based on my 2015 article: Three things I learned from Sandi Metz’s book as a non-Ruby programmer)
What is the single responsibility principle?
A component should have one and only one job. It should do the smallest possible useful thing. It only has responsibilities that fulfill its purpose.
A component with various responsibilities is difficult to reuse. If you want to reuse some but not all of a its behavior, it's almost always impossible to just get what you need. It is also likely to be entangled with other code. Components that do one thing which isolate that thing from the rest of your application allows change without consequence and reuse without duplication.
How to know if your component has a single responsibility?
Try to describe that component in one sentence. If it is only responsible for one thing then it should be simple to describe. If it uses the word ‘and’ or ‘or’ then it is likely that your component fails this test.
Inspect the component's states, the props and hooks it consumes, as well as variables and methods declared inside the component (They shouldn't be too many). Ask yourself: Do these things actually work together to fulfill the component's purpose? If some of them don't, consider moving those somewhere else or breaking down your big component to smaller ones.
The requirement is to display special kinds of buttons you can click to shop for items of a specific category. For example, the user can select bags, chairs, and food.
WavingHand
componentgrey
green
red
type ShopCategoryTileProps = {
isBooked: boolean
icon: ReactNode
label: string
componentInsideModal?: ReactNode
items?: {name: string, quantity: number}[]
}
const ShopCategoryTile = ({
icon,
label,
items
componentInsideModal,
}: ShopCategoryTileProps ) => {
const [openDialog, setOpenDialog] = useState(false)
const [hover, setHover] = useState(false)
const disabled = !items || items.length === 0
return (
<>
<Tooltip title="Not Available" show={disabled}>
<StyledButton
className={disabled ? "grey" : isBooked ? "green" : "red" }
disabled={disabled}
onClick={() => disabled ? null : setOpenDialog(true) }
onMouseEnter={() => disabled ? null : setHover(true)}
onMouseLeave={() => disabled ? null : setHover(false)}
>
{icon}
<StyledLabel>{label}<StyledLabel/>
{!disabled && isBooked && <FaCheckCircle/>}
{!disabled && hover && <WavingHand />}
</StyledButton>
</Tooltip>
{componentInsideModal &&
<Dialog open={openDialog} onClose={() => setOpenDialog(false)}>
{componentInsideModal}
</Dialog>
}
</>
)
}
// split into two smaller components!
const DisabledShopCategoryTile = ({ icon, label }: { icon: ReactNode, label: string }) => {
return (
<Tooltip title="Not available">
<StyledButton disabled={true} className="grey">
{icon} <StyledLabel>{label}<StyledLabel/>
</Button>
</Tooltip>
)
}
type ShopCategoryTileProps = {
isBooked: boolean
icon: ReactNode
label: string
componentInsideModal: ReactNode
}
const ShopCategoryTile = ({
icon,
label,
isBooked,
componentInsideModal,
}: ShopCategoryTileProps ) => {
const [openDialog, setOpenDialog] = useState(false)
const [hover, setHover] = useState(false)
return (
<>
<StyledButton
disabled={false}
className={isBooked ? "green" : "red"}
onClick={() => setOpenDialog(true) }
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{icon}
<StyledLabel>{label}<StyledLabel/>
{isBooked && <FaCheckCircle/>}
{hover && <WavingHand />}
</StyledButton>
<Dialog open={openDialog} onClose={() => setOpenDialog(false)}>
{componentInsideModal}
</Dialog>
</>
)
}
Note: This is a simplified version of a component that I've actually seen in production
const ShopCategoryTile = ({
item,
offers,
}: {
item: ItemMap;
offers?: Offer;
}) => {
const dispatch = useDispatch();
const location = useLocation();
const history = useHistory();
const { items } = useContext(OrderingFormContext);
const [openDialog, setOpenDialog] = useState(false);
const [hover, setHover] = useState(false);
const isBooked =
!item.disabled && !!items?.some((a: Item) => a.itemGroup === item.group);
const isDisabled = item.disabled || !offers;
const RenderComponent = item.component;
useEffect(() => {
if (openDialog && !location.pathname.includes("item")) {
setOpenDialog(false);
}
}, [location.pathname]);
const handleClose = useCallback(() => {
setOpenDialog(false);
history.goBack();
}, []);
return (
<GridStyled
xs={6}
sm={3}
md={2}
item
booked={isBooked}
disabled={isDisabled}
>
<Tooltip
title="Not available"
placement="top"
disableFocusListener={!isDisabled}
disableHoverListener={!isDisabled}
disableTouchListener={!isDisabled}
>
<PaperStyled
disabled={isDisabled}
elevation={isDisabled ? 0 : hover ? 6 : 2}
>
<Wrapper
onClick={() => {
if (isDisabled) {
return;
}
dispatch(push(ORDER__PATH));
setOpenDialog(true);
}}
disabled={isDisabled}
onMouseEnter={() => !isDisabled && setHover(true)}
onMouseLeave={() => !isDisabled && setHover(false)}
>
{item.icon}
<Typography variant="button">{item.label}</Typography>
<CheckIconWrapper>
{isBooked && <FaCheckCircle size="26" />}
</CheckIconWrapper>
</Wrapper>
</PaperStyled>
</Tooltip>
<Dialog fullScreen open={openDialog} onClose={handleClose}>
{RenderComponent && (
<RenderComponent item={item} offer={offers} onClose={handleClose} />
)}
</Dialog>
</GridStyled>
);
};
Avoid premature / inappropriate generalization. If your implementation for a simple feature requires a huge overhead, consider other options. Sandi Metz's article on this is a must read! Sandi Metz: The Wrong Abstraction.
See also: KCD: AHA Programming, C2 Wiki: Contrived Interfaces, C2 Wiki: The Expensive Setup Smell, C2 Wiki: Premature Generalization
Premature optimization is the root of all evil - Tony Hoare (popularized by Donald Knuth)
TL;DR
lazy loading
and bundle/code splitting
useMemo
mostly just for expensive calculationsReact.memo
, useMemo
, useCallback
for reducing re-renders, they shouldn't have many dependencies and the dependencies should be mostly primitive typesReact.memo
, useCallback
and useMemo
is doing what you think it's doing (is it really preventing rerendering?)tannerlinsley/react-virtual
or similar)Context
as low as possible in your component tree. Context
does not have to be global to your whole appContext
should be logically separated, do not add to many values in one context providercontext
by separating the state
and the dispatch
functionreact-hook-forms
. I think it is a great balance of good performance and good developer experience.source-map-explorer
or @next/bundle-analyzer
(for NextJS).useMemo
and useCallback
Write tests. Not too many. Mostly integration. - Guillermo Rauch, creator of Socket.io (and other awesome things)
TL;DR