🥺

Propsをよりスマートに渡したい!

2020/11/08に公開

はじめに

Reactのpropsを渡す方法にフォーカスを当てて、ダサいコードをシュッとしたコードに改善していきます!(語彙力...)

プロジェクトの概要

プロジェクトは大人気マンガONE PIECEの「麦わらの一味」のキャラ名をリストにした簡単なものです。スタイルにはMaterial-Uiを用いています。
麦わらの一味

改善前のファイル構成

├── src/
   ├── components/
           └──Charactor.tsx
           └──CharactorList.tsx
	   └──CharactorHead.tsx
	   
   └── index.tsx
   └──App.tsx

ルートとなるApp.tsxではデフォルトとなるカラーをcreateMuiThemeで表現しています。

App.tsx
import React from 'react';
import { createMuiTheme, ThemeProvider } from '@material-ui/core';
import './App.css';
import Charactor from './components/Charactor';

const theme = createMuiTheme({
  palette: {
    primary: {
      main: '#ff6347',
      light: '#d2b48c',
    },
    background: {
      default: '#2f4f4f',
    },
  },
  overrides: {
    MuiAppBar: {
      root: {
        transform: 'translateZ(0)',
      },
    },
  },
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      <div className="App">
        <header className="App-header">
          <Charactor />
        </header>
      </div>
    </ThemeProvider>
  );
}

export default App;

コンポーネントを利用する側である親コンポーネントはCharacter.tsxファイルとします。

Character.tsx
import {
  makeStyles,
  Paper,
  Table,
  TableBody,
  TableHead,
  Theme,
} from '@material-ui/core';
import React from 'react';
import CharaHead from './CharaHead';
import CharactorList from './CharactorList';

const useStyles = makeStyles((theme:Theme) => ({
  pageContent: {
    backgroundColor: theme.palette.background.default,
    margin: theme.spacing(5),
    padding: theme.spacing(3),
  },
  table: {
    marginTop: theme.spacing(3),
    '& thead th': {
      fontWeight: '600',
      color: theme.palette.primary.main,
    },
    '& tbody td': {
      fontWeight: '400',
    },
    '& tbody tr:hover': {
      backgroundColor: '#fffbf2',
      cursor: 'pointer',
    },
  },
}));

const Charactor = () => {
  const classes = useStyles();

  return (
    <>
      <Paper className={classes.pageContent}>
        <Table className={classes.table}>
          <TableHead>
            <CharaHead
              firstName="firstName"
              middleName="middleName"
              lastName="lastName"
            />
            <TableBody>
              <CharactorList
                firstName="Monkey"
                middleName="D"
                lastName="Luffy"
              />
              <CharactorList
                firstName="Roronoa"
                middleName=""
                lastName="Zoro"
              />
              <CharactorList firstName="" middleName="" lastName="Nami" />
              <CharactorList firstName="" middleName="" lastName="Usopp" />
              <CharactorList
                firstName="Vinsmoke"
                middleName=""
                lastName="Sanji"
              />
              <CharactorList
                firstName="Tony Tony"
                middleName=""
                lastName="Chopper"
              />
              <CharactorList firstName="Nico" middleName="" lastName="Robin" />
              <CharactorList firstName="" middleName="" lastName="Franky" />
              <CharactorList firstName="" middleName="" lastName="Brook" />
              <CharactorList firstName="" middleName="" lastName="Jimbei" />
            </TableBody>
          </TableHead>
        </Table>
      </Paper>
    </>
  );
};

export default Charactor;

またコンポーネントを利用される側である子コンポーネントはCharaHead.tsx、CharactorList.tsxファイルとします。

CharaHead.tsx
import { TableCell, TableHead } from '@material-ui/core';
import React from 'react';

export interface Props {
  firstName: string;
  middleName: string;
  lastName: string;
}

const CharaHead: React.FC<Props> = (props) => {
  const { firstName, middleName, lastName } = props;
  return (
    <TableHead>
      <TableCell> {firstName}</TableCell>
      <TableCell> {middleName}</TableCell>
      <TableCell> {lastName}</TableCell>
    </TableHead>
  );
};

export default CharaHead;
CharacterList.tsx
import { TableRow, TableCell,Theme, makeStyles } from '@material-ui/core';
import React from 'react';

const useStyles = makeStyles((theme:Theme) => ({
  table: {
    color: theme.palette.primary.light,
  },
}));

export interface CharaProps {
  firstName: string;
  middleName: string;
  lastName: string;
}

const CharactorList: React.FC<CharaProps> = (props) => {
  const classes = useStyles();
  const { firstName, middleName, lastName } = props;
  return (
    <TableRow>
      <TableCell className={classes.table} align="center">
        {firstName}
      </TableCell>
      <TableCell className={classes.table} align="center">
        {middleName}
      </TableCell>
      <TableCell className={classes.table} align="center">
        {lastName}
      </TableCell>
    </TableRow>
  );
};

export default CharactorList;

改善後のファイル構成

├── src/
   ├── components/
           └──Charactor.tsx
           └──charaList.ts
	   └──characterType.ts
	   └──useCharacter.tsx
	   
   └── index.tsx
   └──App.tsx

改善すべきポイント①

子コンポーネントを一つの関数としてラップする

改善前では子コンポーネントは2つに分かれていましたが、一つのファイル(useCharacter.tsx)にまとめます。

useCharacter.tsx
import React from 'react';
import { TableHead, TableCell, TableRow } from '@material-ui/core';
import { HeadCells, CharaList } from './charaType';

interface Props {
  firstName: string;
  middleName: string;
  lastName: string;
}

const useCharactor = (headCells: HeadCells, charaLists: CharaList) => {
  const CharaHeads: React.FC = () => {
    return (
      <TableHead>
        {headCells.map((headCell) => (
          <TableCell key={headCell.id}>{headCell.label}</TableCell>
        ))}
      </TableHead>
    );
  };

  const CharaLists: React.FC<Props> = (props) => {
    const { firstName, middleName, lastName } = props;
    return (
      <TableRow>
        <TableCell align="center">{firstName}</TableCell>
        <TableCell align="center">{middleName}</TableCell>
        <TableCell align="center">{lastName}</TableCell>
      </TableRow>
    );
  };

  return { CharaHeads, CharaLists };
};

export default useCharactor;

useCharacter関数の中で子コンポーネントを作成し、返り値としてその子コンポーネントを持っているといった具合です。
また、アロー関数の中身にreturn文しかない場合はreturnを省略できます。

シンプルにしたuseCharacter.tsxの一部
const useCharacter = (headCells: HeadCells, charaLists: CharaList) => {
  const CharaHeads: React.FC = () => (
    <TableHead>
      {headCells.map((headCell) => (
        <TableCell key={headCell.id}>{headCell.label}</TableCell>
      ))}
    </TableHead>
  );

  const CharaLists: React.FC<Props> = ({ firstName, middleName, lastName }) => (
    <TableRow>
      <TableCell align="center">{firstName}</TableCell>
      <TableCell align="center">{middleName}</TableCell>
      <TableCell align="center">{lastName}</TableCell>
    </TableRow>
  );

  return { CharaHeads, CharaLists };
};

export default useCharacter;

useCharacter関数の2つの引数であるheadCellsとcharaListsは別途ファイルに分けて、親コンポーネントであるCharacter.tsxに受け渡します。

charaLists.ts
export const headCells = [
  { id: 0, label: 'firstName' },
  { id: 1, label: 'middleName' },
  { id: 2, label: 'lastName' },
];

export const charaLists = [
  { firstName: 'Monkey', middleName: 'D', lastName: 'Luffy' },
  { firstName: 'Roronoa', middleName: '', lastName: 'Zoro' },
  { firstName: '', middleName: '', lastName: 'Nami' },
  { firstName: '', middleName: '', lastName: 'Usopp' },
  { firstName: 'Vinsmoke', middleName: '', lastName: 'Sanji' },
  { firstName: 'Tony Tony', middleName: '', lastName: 'Chopper' },
  { firstName: 'Nico', middleName: '', lastName: 'Robin' },
  { firstName: '', middleName: '', lastName: 'Franky' },
  { firstName: '', middleName: '', lastName: 'Brook' },
  { firstName: '', middleName: '', lastName: 'Jimbei' },
];

改善すべきポイント②

propsを配列処理として渡す

改善前の親コンポーネントのコードではpropsを一つ一つ渡していましたが、配列処理(mapメソッド)を用いてよりスマートにpropsを渡すよう改善しました。

Character.tsx
import {
  makeStyles,
  Paper,
  Table,
  TableBody,
  TableHead,
} from '@material-ui/core';
import React from 'react';
import { headCells, charaLists } from './charaList';
import useCharacter from './useCharacter';

const useStyles = makeStyles((theme) => ({
  pageContent: {
    backgroundColor: theme.palette.background.default,
    margin: theme.spacing(5),
    padding: theme.spacing(3),
  },
  table: {
    marginTop: theme.spacing(3),
    '& thead th': {
      fontWeight: '600',
      color: theme.palette.primary.main,
    },
    '& tbody td': {
      fontWeight: '400',
      color: theme.palette.primary.light,
    },
    '& tbody tr:hover': {
      backgroundColor: '#fffbf2',
      cursor: 'pointer',
    },
  },
}));

const Character = () => {
  const classes = useStyles();
  const { CharaHeads, CharaLists } = useCharacter(headCells, charaLists);

  return (
    <>
      <Paper className={classes.pageContent}>
        <Table className={classes.table}>
          <TableHead>
            <CharaHeads />
            <TableBody>
              {charaLists.map((charaList) => {
                const { firstName, middleName, lastName } = charaList;
                return (
                  <CharaLists
                    firstName={firstName}
                    middleName={middleName}
                    lastName={lastName}
                  ></CharaLists>
                );
              })}
            </TableBody>
          </TableHead>
        </Table>
      </Paper>
    </>
  );
};

export default Character;

返り値のJSX.Elementを比較してもらえれば一目瞭然ですが、かなりコードの量が削減されています。

親コンポーネントでは、子コンポーネントのラッパー関数であるuseCharacterから分割代入を用いて子コンポーネントを取り出しています。後は子コンポーネントをJSX.Elementとして適切な場所に配置するといった具合です。

最後に親コンポーネントの記述を更に簡潔に表記できるため、そちらをお見せしてこの記事を締めくくりたいと思います。

Character.tsxの一部
const Character = () => {
  const classes = useStyles();
  const { CharaHeads, CharaLists } = useCharacter(headCells, charaLists);

  return (
    <>
      <Paper className={classes.pageContent}>
        <Table className={classes.table}>
          <TableHead>
            <CharaHeads />
            <TableBody>
              {charaLists.map((charaList) => {
                return <CharaLists {...charaList}></CharaLists>;
              })}
            </TableBody>
          </TableHead>
        </Table>
      </Paper>
    </>
  );
};

export default Character;

改善を行ったのは子コンポーネントであるCharaListsを配列処理で表示させている部分です。mapメソッドの引数(コールバック関数)の更にその引数であるcharaListにはfirstName,middleName,lastNameをプロパティーにもつオブジェクトが内包されています。そのcharaListをCharaListsコンポーネントの属性としてスプレッド構文を利用することで、さらにコードを簡潔に書くことを可能にしています。

以上になります。ここまで読んでいただきありがとうございました!!

Discussion