本記事は React best practices and patterns to reduce code - Part3 を提供元の事前許可を得たうえで翻訳したものです。
元の記事に従いタイトルに「ベストプラクティス」と含んでいますが、実際にはベストプラクティスは規模や状況によって大きく異なります。
チームの状況にあわせて参考にしていただければと思います。
=====
これは全3パート中の最後である第3パートとなる記事です。前2つの記事を読んでいなければ是非以下のリンクからお読みください。
それではいきましょう。
トークンはlocalStorageよりもCookieに保存する
よくないコード:
const token = localStorage.getItem("token");
if (token) {
axios.defaults.headers.common["Authorization"] = token;
}
よいコード:
import Cookies from "js-cookie"; // use another library if you want
const token = Cookies.get("token");
if (token) {
axios.defaults.headers.common["Authorization"] = token;
}
よりよいコード:
No Code 😉
注釈:
- Cookieはサブドメインも含んだサイトで共有されますが、リクエスト毎にトークンを渡す必要はありません。なお、バックエンドがフロントエンドと同じドメインではない場合は2つ目の方法を取る必要があります。
-
HttpOnly
属性を使うとJavaScriptからCookieの値にアクセスできなくなります。そこでReactではアクセス可能であることをチェックするためのフラグが必要になることもあるでしょう。
訳者注:
トークンの保存についてはCookie / LocalStorageそれぞれについて様々議論されており全てのケースで一概に○○の方法が良い、とは言い切れません。上記はあくまで一つの意見として理解しておく程度で良いと感じました。
認証トークンや共通ヘッダの付与にはinterceptorsを使用する
よくないコード:
axios.get("/api", {
headers: {
ts: new Date().getTime(),
},
});
よいコード:
// only once
axios.interceptors.request.use(
(config) => {
// Do something before request is sent
config.headers["ts"] = new Date().getTime();
return config;
},
(error) => {
// Do something with request error
return Promise.reject(error);
}
);
// Component
axios.get("/api");
propsを子要素に渡すためにcontext/reduxを使用する
よくないコード:
const auth = { name: "John", age: 30 };
return (
<Router>
<Route path="/" element={<App auth={auth} />} />
<Route path="/home" element={<Home auth={auth} />} />
</Router>
);
よいコード:
return (
<Provider store={store}>
<Router>
<Route
path="/"
element={<App />}
/>
<Route
path="/home"
element={<Home />}
/>
</Router>
);
// Inside child component
const { auth } = useContext(AuthContext); // For context
const { auth } = useSelector((state) => state.auth); // For redux
styled-componentのためのヘルパー関数を作成する
悪くはないがピクセルで考えることが非常に難しいコード:
const Button = styled.button`
margin: 1.31rem 1.43rem;
padding: 1.25rem 1.5rem;
`;
ピクセルをremに変換するヘルパー関数を作成:
const toRem = (value) => `${value / 16}rem`;
const Button = styled.button`
margin: ${toRem(21)} ${toRem(23)};
padding: ${toRem(20)} ${toRem(24)};
`;
input要素が変更された際のための共通関数を用意する
よくないコード:
const onNameChange = (e) => setName(e.target.value);
const onEmailChange = (e) => setEmail(e.target.value);
return (
<form>
<input type="text" name="name" onChange={onNameChange} />
<input type="text" name="email" onChange={onEmailChange} />
</form>
);
よいコード:
const onInputChange = (e) => {
const { name, value } = e.target;
setFormData((prevState) => ({
...prevState,
[name]: value,
}));
};
return (
<form>
<input type="text" name="name" onChange={onInputChange} />
<input type="text" name="email" onChange={onInputChange} />
</form>
);
遅延ロードのためにintersection observerを使用する
よくないコード:
element.addEventListener("scroll", function (e) {
// do something
});
よいコード:
const useScroll = (ele, options = {}): boolean => {
const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => {
const cb = (entry) => setIsIntersecting(() => entry.isIntersecting);
const callback: IntersectionObserverCallback = (entries) => entries.forEach(cb);
const observer = new IntersectionObserver(callback, options);
if (ele) observer.observe(ele);
return (): void => ele && observer.unobserve(ele);
}, [ele]);
return isIntersecting;
};
// Component
const ref = useRef<any>();
const isIntersecting = useScroll(ref?.current);
useEffect(() => {
if (isIntersecting) {
// call an API
}
}, [isIntersecting]);
認証ページや内部的なルーティングのためにHOC(高階コンポーネント)を使用する
よくないコード:
const Component = () => {
if (!isAuthenticated()) {
return <Redirect to="/login" />;
}
return <div></div>;
};
よいコード:
const withAuth = (Component) => {
return (props) => {
if (!isAuthenticated()) {
return <Redirect to="/login" />;
}
return <Component {...props} />;
};
};
// Route
<Route path="/home" component={withAuth(Home)} />;
// Component
const Component = (props) => <div></div>;
export default withAuth(Component);
ルーティング定義のために配列でデータ管理をする
よくないコード:
return (
<Router>
<Route path="/" element={<App />} />
<Route path="/about" element={<About />} />
<Route path="/topics" element={<Topics />} />
</Router>
);
よいコード:
const routes = [
{
path: "/",
role: ["ADMIN"],
element: React.lazy(() => import("../pages/App")),
children: [
{
path: "/child",
element: React.lazy(() => import("../pages/Child")),
},
],
},
{
path: "/about",
role: [],
element: React.lazy(() => import("../pages/About")),
},
{
path: "/topics",
role: ["User"],
element: React.lazy(() => import("../pages/Topics")),
},
];
const createRoute = ({ element, children, role, ...route }) => {
const Component = role.length > 0 ? withAuth(element) : element;
return (
<Route key={route.path} {...route} element={<Component />}>
{children && children.map(createRoute)}
</Route>
);
};
return <Routes>{routes.map(createRoute)}</Routes>;
注釈:より多くのコードは必要になりますが、それ以上に柔軟性が上がります。仮にさらにHOCを導入する場合でも変更が必要なのはcreateRouteメソッド内部のみです。
TypeScriptを使う
TypeScriptを使わなくても悪いことは何もありませんが、より良いコード記述を助けてくれるでしょう:
npx create-react-app my-app --template typescript
eslintやprettierをフォーマットに利用する
npm install -D eslint prettier
npx eslint --init
参照:Eslint setup、Prettier Setup
😥この記事はシンプルかつ短く保っておきたいため、全ての導入手順は記載していません。
eslintやprettierをフォーマットに利用する pre-commitフックを使ってeslintやprettierを動作させる
npx mrm@2 lint-staged // This will install and configure pre-commit hook
// This script will be created at the root of your project
.husky/pre-commit
// Package.json
"lint-staged": {
"src/**/*.{js,ts,jsx,tsx}": [
"npm run lint",
"npm run prettier",
"npm run unit-test",
"git add"
]
}
注釈:
- コミットの際にprettierやeslintを動作させるような設定が可能です。手元のプロジェクトのpackage.jsonで記述を調整してください。
- CIやCDでこういった自動化設定を行うことがより良く、その場合はpre-commitをコメントアウトしてgitにコードをプッシュできるでしょう。
より良い開発のためにVSCodeの拡張を活用する
Auto Close Tag, Auto Rename Tag, CodeMetrics, CSS Peek, ES7+ React/Redux/React-Native snippets, Eslint, GitLens, Import Cost, Prettier
注釈:CodeMetricsなどのコード複雑度のための拡張はぜひ試してみてください。コードの複雑度を表示し、より良いコードを書くためのサポートをしてくれます。
ここまでお読みいただきありがとうございました😊
--
今後もWebフロントエンドにまつわる最新情報を発信予定です。
興味のある方はぜひ 公式Twitter をフォローしてください。