visit
Why do even experienced developers fall into the trap of anti-patterns?
Hint #1: Hook without a dependency array
Hence, be cautious when you are using hooks such as useState
, useRef
etc., because they do not take dependencies arrays.
Hint #2: Nesting over the composition
The desired behavior of the app
(num_chars(title) + num_chars(text)
and displayed.
Building the app
Code structure
The src/pages
directory has pages mapped to each step. The file for each page in src/pages
contains a ArticleContent
component. The code under discussion is inside this ArticleContent
component. To follow along, you may check the corresponding file in the sandbox or refer the code snippet attached.
Anti-Pattern #1: Props or context as initial state
In the incorrect approach, props
or context
has been used as an initial value for useState
or useRef
. In line 21 of Incorrect.tsx
, we can see that the total character count has been calculated and stored as a state.
import { useCallback, useEffect, useState } from "react";
import { useGetArticles } from "../hooks/useGetArticles";
import { useGetEmoji } from "../hooks/useGetEmoji";
import { Articles } from "../types";
import { Navigation } from "../components/Navigation";
const styles: { [key: string]: React.CSSProperties } = {
container: {
background: "#FEE2E2",
height: "100%",
display: "grid",
gridTemplateColumns: "10rem auto"
},
content: {}
};
const ArticleContent: React.FC<{
article: Articles["articles"]["0"];
}> = (props) => {
// Step 1. calculate length as we need it to get corresponding emotion
const [length] = useState<number>(
props.article.text.length + props.article.title.length
);
// Step 2. fetch emotion map from backend
const emotions = useGetEmoji();
// Step 3. set emotion once we get emotion map from backend
const [emotion, setEmotion] = useState<string>("");
useEffect(() => {
if (emotions) {
setEmotion(emotions["stickers"][length]);
}
}, [emotions, length]);
return (
<div>
<div>
<h2>{props.article.title}</h2>
<div>{props.article.text}</div>
</div>
<h3
dangerouslySetInnerHTML={{
__html: `Total Length ${length} ${emotion}`
}}
/>
</div>
);
};
const Incorrect: React.FC = () => {
const articles = useGetArticles();
const [currentArticle, setCurrentArticle] = useState<
Articles["articles"]["0"] | null
>();
const onClickHandler = useCallback((article) => {
setCurrentArticle(article);
}, []);
return (
<div style={styles.container}>
<Navigation articles={articles} onClickHandler={onClickHandler} />
<div style={styles.content}>
{currentArticle ? <ArticleContent article={currentArticle} /> : null}
</div>
</div>
);
};
export default Incorrect;
Anti-Pattern #2: Destroy and Recreate
Note that a parent component can use the key
prop to destroy the component and recreate it every time the key
changes. Yes, you read it right - you can use keys outside loops.
Specifically, we implement ‘Destroy and Recreate’ anti-pattern by using the key
prop while rendering the child component ArticleContent
of the parent component PartiallyCorrect
in the PartiallyCorrect.tsx
file (line 65).
import { useCallback, useEffect, useState } from "react";
import { Navigation } from "../components/Navigation";
import { useGetArticles } from "../hooks/useGetArticles";
import { useGetEmoji } from "../hooks/useGetEmoji";
import { Articles } from "../types";
const styles: { [key: string]: React.CSSProperties } = {
container: {
background: "#FEF2F2",
height: "100%",
display: "grid",
gridTemplateColumns: "10rem auto"
},
content: {}
};
const ArticleContent: React.FC<{
article: Articles["articles"]["0"];
}> = (props) => {
// Step 1. calculate length as we need it to get corresponding emotion
const [length] = useState<number>(
props.article.text.length + props.article.title.length
);
// Step 2. fetch emotion map from backend
const emotions = useGetEmoji();
// Step 3. set emotion once we get emotion map from backend
const [emotion, setEmotion] = useState<string>("");
useEffect(() => {
if (emotions) {
setEmotion(emotions["stickers"][length]);
}
}, [emotions, length]);
return (
<div>
<div>
<h2>{props.article.title}</h2>
<div>{props.article.text}</div>
</div>
<h3
dangerouslySetInnerHTML={{
__html: `Total Length ${length} ${emotion}`
}}
/>
</div>
);
};
const PartiallyCorrect: React.FC = () => {
const articles = useGetArticles();
const [currentArticle, setCurrentArticle] = useState<
Articles["articles"]["0"] | null
>();
const onClickHandler = useCallback((article) => {
setCurrentArticle(article);
}, []);
return (
<div style={styles.container}>
<Navigation articles={articles} onClickHandler={onClickHandler} />
<div style={styles.content}>
{/** Step 4. Using key to force destroy and recreate */}
{currentArticle ? (
<ArticleContent article={currentArticle} key={currentArticle.id} />
) : null}
</div>
</div>
);
};
export default PartiallyCorrect;
Pattern #1: Internal State in JSX
To implement ‘re-rendering’, useEffect
and useState
will be used in tandem. The initial value for the useState
can be set to null
or undefined
and an actual value will be computed and assigned to it once useEffect
has run. In this pattern, we are circumventing the lack of dependency array in useState
by using useEffect
.
Specifically, notice how we have moved the total character count computation into JSX (line 44) in the Suboptimal.tsx
and we are using props
(line 33) as a dependency in the useEffect
(line 25). Using this pattern, the flickering effect has been avoided but a network request is made to fetch emojis whenever props change. So even if there is no change in character count, an unnecessary request is made to fetch the same emoji.
import { useCallback, useEffect, useState } from "react";
import { Navigation } from "../components/Navigation";
import { useGetArticles } from "../hooks/useGetArticles";
import { useGetEmoji } from "../hooks/useGetEmoji";
import { Articles } from "../types";
const styles: { [key: string]: React.CSSProperties } = {
container: {
background: "#FEFCE8",
height: "100%",
display: "grid",
gridTemplateColumns: "10rem auto"
},
content: {}
};
const ArticleContent: React.FC<{
article: Articles["articles"]["0"];
}> = (props) => {
// Step 2. fetch emotion map from backend
const emotions = useGetEmoji();
// Step 3, set emotion once we get emotion map from backend
const [emotion, setEmotion] = useState<string>("");
useEffect(() => {
if (emotions) {
setEmotion(
emotions["stickers"][
props.article.text.length + props.article.title.length
]
);
}
}, [emotions, props]);
return (
<div>
<div>
<h2>{props.article.title}</h2>
<div>{props.article.text}</div>
</div>
<h3
dangerouslySetInnerHTML={{
__html: `Total Length ${
props.article.text.length + props.article.title.length
} ${emotion}`
}}
/>
</div>
);
};
const Suboptimal: React.FC = () => {
const articles = useGetArticles();
const [currentArticle, setCurrentArticle] = useState<
Articles["articles"]["0"] | null
>();
const onClickHandler = useCallback((article) => {
setCurrentArticle(article);
}, []);
return (
<div style={styles.container}>
<Navigation articles={articles} onClickHandler={onClickHandler} />
<div style={styles.content}>
{currentArticle ? <ArticleContent article={currentArticle} /> : null}
</div>
</div>
);
};
export default Suboptimal;
Pattern #2: Props as a dependency in useMemo
Let us do it correctly as well as optimally this time. It all started with Anti-Pattern #1: props
or context
as initial state.
We can fix this by using props
as a dependency in useMemo
. By moving the total character count computation to useMemo
hook in Optimal.tsx
(line 22), we are able to prevent network request to fetch emoji unless the total character count has changed.
import { useCallback, useEffect, useMemo, useState } from "react";
import { Navigation } from "../components/Navigation";
import { useGetArticles } from "../hooks/useGetArticles";
import { useGetEmoji } from "../hooks/useGetEmoji";
import { Articles } from "../types";
const styles: { [key: string]: React.CSSProperties } = {
container: {
background: "#F0FDF4",
height: "100%",
display: "grid",
gridTemplateColumns: "10rem auto"
},
content: {}
};
const ArticleContent: React.FC<{
article: Articles["articles"]["0"];
}> = (props) => {
// Step 1. calculate length as we need it to get corresponding emotion
const length = useMemo<number>(
() => props.article.text.length + props.article.title.length,
[props]
);
// Step 2. fetch emotion map from backend
const emotions = useGetEmoji();
// Step 3. set emotion once we get emotion map from backend
const [emotion, setEmotion] = useState<string>("");
useEffect(() => {
if (emotions) {
setEmotion(emotions["stickers"][length]);
}
}, [emotions, length]);
return (
<div>
<div>
<h2>{props.article.title}</h2>
<div>{props.article.text}</div>
</div>
<h3
dangerouslySetInnerHTML={{
__html: `Total Length ${length} ${emotion}`
}}
/>
</div>
);
};
const Optimal: React.FC = () => {
const articles = useGetArticles();
const [currentArticle, setCurrentArticle] = useState<
Articles["articles"]["0"] | null
>();
const onClickHandler = useCallback((article) => {
setCurrentArticle(article);
}, []);
return (
<div style={styles.container}>
<Navigation articles={articles} onClickHandler={onClickHandler} />
<div style={styles.content}>
{currentArticle ? <ArticleContent article={currentArticle} /> : null}
</div>
</div>
);
};
export default Optimal;
In this article, we discussed that using props
or context
as initial state and ‘Destroy and Recreate’ are anti-patterns while using internal state in JSX and props
as a dependency in useMemo are good patterns. We also learned that we should be cautious when we are using hooks without a dependency array and nesting for arranging React components.