component

保持頂層 (Top-Level) 宣告

// ❌
const App = () => {
  const Container = ({ children }) => <section>{children}</section>;
  const Button = ({ onClick }) => <button onClick={onClick}>Click Me!</button>;

  const handleClick = () => {
    // do something...
  };

  return (
    <Container>
      <Button onClick={handleClick} />
    </Container>
  );
};

// ✅
const Container = ({ children }) => <section>{children}</section>;
const Button = ({ onClick }) => <button onClick={onClick}>Click Me!</button>;

const App = () => {
  const handleClick = () => {
    // do something...
  };

  return (
    <Container>
      <Button onClick={handleClick} />
    </Container>
  );
};

盡量描述 UI 的形狀,Component 化

const [HOME, PROFILE, SETTINGS] = ['home', 'profile', 'settings'];
const pages = {
  [HOME]: () => <Home />,
  [PROFILE]: () => <Profile />,
  [SETTINGS]: () => <Settings />,
};

// ❌
const App = () => {
  const history = useHistory();
  const { pathname } = useLocation();

  const routeToMenu = (type) => () => {
    history.push(`/${type}`);
  };

  const renderContent = () => {
    return pages[pathname]();
  };

  return (
    <section>
      <div>
        <button>Sign In</button>
        <button>Sign Up</button>
        <button>Forget Password</button>
      </div>
      <div>
        <div onClick={routeToMenu(HOME)}></div>
        <div onClick={routeToMenu(PROFILE)}></div>
        <div onClick={routeToMenu(SETTINGS)}></div>
      </div>
      <div>{renderContent()}</div>
    </section>
  );
};

// ✅ (描述 UI 的形狀,讓開發人員第一眼就可以看得出藍圖)
// 一個 Component file 盡量保持一個 Component 就好
// App.jsx
const App = () => {
  return (
    <Container>
      <Toolbar />
      <Menus />
      <Page />
    </Container>
  );
};

// Container.jsx
const Container = ({ children }) => <section>{children}</section>;

// Toolbar.jsx
const Toolbar = () => (
  <div>
    <button>Sign In</button>
    <button>Sign Up</button>
    <button>Forget Password</button>
  </div>
);

// Menus.jsx
const Menus = () => {
  const history = useHistory();

  const routeToMenu = (type) => () => {
    history.push(`/${type}`);
  };

  return (
    <div>
      <div onClick={routeToMenu(HOME)}></div>
      <div onClick={routeToMenu(PROFILE)}></div>
      <div onClick={routeToMenu(SETTINGS)}></div>
    </div>
  );
};

// Page.jsx
const Page = () => {
  const { pathname } = useLocation();

  const renderContent = () => {
    return pages[pathname]();
  };

  return <div>{renderContent()}</div>;
};

使用到 forwardRef 得描述 displayName

(否則 React Component DevTools 會抓不到該 Component 名稱,難以 Debug)

// ❌
const Button = forwardRef(({ onClick }, ref) => (
  <button ref={ref} onClick={onClick}>
    Click Me!
  </button>
));

// ✅
const Button = forwardRef(({ onClick }, ref) => (
  <button ref={ref} onClick={onClick}>
    Click Me!
  </button>
));

Button.displayName = 'Button';

請勿違反設計原則

// ❌
class Content extends Component {
  constructor(props) {
    super(props);
    this.state = {
      open: false,
    };
  }

	// eslint 會提示 unused function,此時拔除會壞掉
  openDialog = () => this.setState({ open: true });

  render() {
    return (
      <Fragment>
        <Title>content-title</Title>
        <Dialog open={this.state.open}>
          <DialogTitle>dialog-title</DialogTitle>
          <DialogContent>dialog-content</DialogContent>
          <DialogFooter>dialog-footer</DialogFooter>
        </Dialog>
      </Fragment>
    );
  }
}

class Page extends Component {
  constructor(props) {
    super(props);
    this.dialogRef = createRef();
  }

  handleOpenDialog = () => {
    this.dialogRef.openDialog();
  };

  render() {
    return (
      <div>
        <Content ref={this.dialogRef} />
        <button onClick={this.handleOpenDialog}>open dialog</button>
      </div>
    );
  }
}

// ✅
class Content extends Component {
  render() {
    return (
      <Fragment>
        <Title>content-title</Title>
        <Dialog open={this.props.dialogOpen}>
          <DialogTitle>dialog-title</DialogTitle>
          <DialogContent>dialog-content</DialogContent>
          <DialogFooter>dialog-footer</DialogFooter>
        </Dialog>
      </Fragment>
    );
  }
}

class Page extends Component {
  constructor(props) {
    super(props);
    this.state = {
      dialogOpen: false,
    };
  }

  handleOpenDialog = () => {
    this.setState({ dialogOpen: true });
  };

  render() {
    return (
      <div>
        <Content dialogOpen={this.dialogOpen} />
        <button onClick={this.handleOpenDialog}>open dialog</button>
      </div>
    );
  }
}

少使用 lodash,以原生 api 為主

const value = {
  0: 123,
  1: 456,
  2: 789,
};

// ❌ 使用 lodash reduce,無法第一眼就看出 value 是哪種資料型態
const sum = _.reduce(value, (acc, cur) => acc + cur, 0);

// ✅ 使用 ES6 Object.values,即可得知 value 為物件型態
const sum = Object.values(value).reduce((acc, cur) => acc + cur, 0);

props default value 設置

const InputLabel = ({ id, value }) => (
  <div>
    <label htmlFor={id}>{id}</label>
    <input type="text" value={value} readOnly />
  </div>
);

// ❌ class component 若要給定 props default value
// 需綁定在 defaultProps 上
class Contact extends Component {
  render() {
    const { profile = {} } = this.props;
    const { id = '', name = '', address = '' } = profile;
    return (
      <div>
        <InputLabel id="id" value={id} />
        <InputLabel id="name" value={name} />
        <InputLabel id="address" value={address} />
      </div>
    );
  }
}

// ✅
class Contact extends Component {
  render() {
    const {
      profile: { id, name, address },
    } = this.props;
    return (
      <div>
        <InputLabel id="id" value={id} />
        <InputLabel id="name" value={name} />
        <InputLabel id="address" value={address} />
      </div>
    );
  }
}

Contact.propTypes = {
  profile: PropTypes.shape({
    id: PropTypes.string,
    name: PropTypes.string,
    address: PropTypes.string,
  }),
};

Contact.defaultProps = {
  profile: {
    id: '',
    name: '',
    address: '',
  },
};

// ✅ function component 若要給定 props default value
// 直接宣告在上方即可
const Contact = ({ profile: { id = '', name = '', address = '' } = {} }) => (
  <div>
    <InputLabel id="id" value={id} />
    <InputLabel id="name" value={name} />
    <InputLabel id="address" value={address} />
  </div>
);

Contact.propTypes = {
  profile: PropTypes.shape({
    id: PropTypes.string,
    name: PropTypes.string,
    address: PropTypes.string,
  }),
};

callback function 描述

// ❌ 若是多行的 callback function,則需另外定義變數描述此行為
// 否則無法第一眼看出此 Component 的行為
const ButtonGroups = ({ onCancel }) => {
  return (
    <div>
      <Button
        onClick={() => {
          fetch('https://gorilla-sc/create')
            .then((res) => res.json())
            .then(() => {
              alert('successfully created !!');
            });
        }}
      >
        Create
      </Button>
      <Button
        onClick={() => {
          fetch('https://gorilla-sc/read')
            .then((res) => res.json())
            .then((data) => {
              console.log('read data: ', data);
            });
        }}
      >
        Read
      </Button>
      <Button onClick={() => onCancel()}>Cancel</Button>
    </div>
  );
};

// ✅ 描述該 callback function 行為,使 Component 更容易閱讀
// 若是接過來的 props callback function 則以 "on" 為字首描述
const ButtonGroups = ({ onCancel }) => {
  // 若 callback function 權責在 parent 手上,則以 "handle" 為字首描述
  const handleCreate = () => {
    fetch('https://gorilla-sc/create')
      .then((res) => res.json())
      .then(() => {
        alert('successfully created !!');
      });
  };

  const handleRead = () => {
    fetch('https://gorilla-sc/read')
      .then((res) => res.json())
      .then((data) => {
        console.log('read data: ', data);
      });
  };

  return (
    <div>
      <Button onClick={handleCreate}>Create</Button>
      <Button onClick={handleRead}>Read</Button>
      {/** callback function 只有一行,則可不需要再加以描述,因為「第一眼」看得出來 */}
      {/** 按鈕 => onClick => 觸發 onCancel function */}
      <Button onClick={() => onCancel()}>Cancel</Button>
    </div>
  );
};

useState

state 以及 setState 命名必須配對

 const [filterText, setFilterText] = useState('')
 const [filterText, setText] = useState('')

若有搭配條件式判定,則 state 描述須確實 (verb. + adj.)

 const [isWaiting, setIsWaiting] = useState(false);
 const [waiting, setWaiting] = useState(false);

 if (isWaiting) { // do something } => 若等待中,則 ... => 語意正確
 if (waiting) { // do something } => 若等待,則 ... => 語意不正確

勿過度裝載 props 當作 state,這會讓 Component 變複雜

// ✅
const FilterableProductTable = ({ products }) => (
	<div>
    <SearchBar />
    <ProductTable products={products} />
  </div>
);

// ❌
const FilterableProductTable = ({ products }) => {
  const [currentProduct, setCurrentProduct] = useState(products);
  return (
    <div>
      <SearchBar />
      <ProductTable products={currentProduct} />
    </div>
  )
}; 

若為 Object 或 Array,則必須遵照 immutable 規範

// state
const [contactInfos, setContactInfos] = useState({
	name: '',
  email: '',
	address: '',
})

const handleUpdateContactInfos = (e) => {
  // ❌
  contactInfos[e.target.name] = e.target.value;
  setContactInfos(contactInfos);

  // ✅ (使用 ES6 spread operator 以 shallow copy)
	setContactInfos(prev => ({
     ...prev,
     [e.target.name]: e.target.value,
  });

  // ✅ (使用 immer library,則須保證此值不會被其他地方 mutable 到,
  // 否則可能會壞掉 (Auto Freeze))
  setContactInfos(prev => {
    const newContactInfos = produce(prev, draft => {
      draft[e.target.name] = e.target.value,
    });
    return newContactInfos;
  })
};


// props
// ❌ 這樣會改變到 data 這個物件的源頭
const App = ({ data }) => {
  data.person = 'tom';
  data.phone = '0912355678';

  return <Contact {...data} />;
};

// ✅ 使用 ES6 spread 攤平並直接定義在 props 上
const App = ({ data }) => (
  <Contact  
    {...data}
    person="tom"
    phone="0912355678"
  />
);

// ✅ 使用 ES6 spread 做 shallow copy 宣告新的 props
const App = ({ data }) => {
  const newData = {
    ...data,
    person: 'tom',
    phone: '0912355678',
  };

  return <Contact {...newData} />;
};

// ✅ 使用 immer library 宣告新的 props
const App = ({ data }) => {
  const newData = produce(data, draft => {
    draft.person = 'tom'
    draft.phone = '0912355678'
  })
  
  return <Contact {...newData} />;
};

Lift State Up or Down / Move Content Up

const ExpensiveTree = () => {
  const now = performance.now();

  while (performance.now() - now < 100) {
    // 故意延遲,不做任何事情而等待 100ms
  }

  return <p>我是一個非常慢的 component tree</p>;
};

// ❌ setColor 後的 render 影響到 ExpensiveTree 造成無謂的渲染
const App = () => {
  const [color, setColor] = useState('red');

  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
};

// ✅ 方法一:把 state 往下搬移,即可解決多於渲染問題
// (因為往下搬移後,只有 InputParagraph 這個 Component 會 render)
const InputParagraph = () => {
  const [color, setColor] = useState('red');

  return (
    <>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
    </>
  );
};

const App = () => {
  return (
    <div>
      <InputParagraph />
      <ExpensiveTree />
    </div>
  );
};

// ✅ 方法二:把 ExpensiveTree 變成 children
// 對 React 而言,children 的異動是它偵測不到的
// 所以 ExpensiveTree 只 render 一次,並且還能避掉之後 setColor 導致的 render 行為
const Layout = ({ children }) => {
  const [color, setColor] = useState('red');

  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      {children}
    </div>
  );
};

const App = () => {
  return (
    <Layout>
      <ExpensiveTree />
    </Layout>
  );
};

useEffect

有時 effect 是不必要的

// ❌
const App = () => {
  const [data, setData] = useState(null);
  const [loadKey, setLoadKey] = useState(null);

  useEffect(() => {
    if (loadKey) {
      fetch('https://www.test.com')
        .then((res) => res.json())
        .then((data) => setData(data));
    }
  }, [loadKey]);

  const handleGetData = () => {
    setLoadKey(Math.random());
  };

  return (
    <div>
      <button onClick={handleGetData}>get data</button>
      {data}
    </div>
  );
};

// ✅
const App = () => {
  const [data, setData] = useState(null);

  const handleGetData = () => {
    fetch('https://www.test.com')
      .then((res) => res.json())
      .then((data) => setData(data));
  };

  return (
    <div>
      <button onClick={handleGetData}>get data</button>
      {data}
    </div>
  );
};

dependencies 越少越好

(當一個 effect dependencies 很多的時候,很容易造成閱讀上的困難)

// ❌
useEffect(() => {
  const checkIdurl = `https://www.movie-ticket.com/id=${id}`;

  fetch(checkIdurl)
    .then((res) => res.json())
    .then((data) => {
      if (!data.isLegal) alert('identified error');
      else {
        const orderTicketUrl = `https://www.movie-ticket.com/id=${id}&name=${name}&date=${date}&people=${people}&theatre=${theatre}`;

        fetch(orderTicketUrl, { method: 'POST' })
          .then((res) => res.json())
          .then((data) => {
            if (data.success) alert('order successfully');
            else alert('order failed');
          });
      }
    });
}, [id, name, date, people, theatre, movieId]);

// ✅ (可透過 useCallback 分離關注點並加以描述,使 effect 更容易閱讀,各司其職)
const orderTicket = useCallback(() => {
  const url = `https://www.movie-ticket.com/id=${id}&movieId=${movieId}&date=${date}&people=${people}&theatre=${theatre}`;

  fetch(url, { method: 'POST' })
    .then((res) => res.json())
    .then((data) => {
      if (data.success) alert('order successfully');
      else alert('order failed');
    });
}, [id, movieId, date, people, theatre]);

const verifyIdToOrderTicket = useCallback(async () => {
  const url = `https://www.movie-ticket.com/id=${id}`;

  fetch(url, { method: 'POST' })
    .then((res) => res.json())
    .then((data) => {
      if (data.isLegal) orderTicket();
      else alert('identified error');
    });
}, [id, orderTicket]);

useEffect(() => {
  verifyIdToOrderTicket();
}, [verifyIdToOrderTicket]);

// ✅ (或至少針對 effect 內容加以描述,使其更容易閱讀)
useEffect(() => {
  const orderTicket = () => {
    const url = `https://www.movie-ticket.com/id=${id}&name=${name}&date=${date}&people=${people}&theatre=${theatre}`;

    fetch(url, { method: 'POST' })
      .then((res) => res.json())
      .then((data) => {
        if (data.success) alert('order successfully');
        else alert('order failed');
      });
  };

  const verifyIdToOrderTicket = () => {
    const url = `https://www.movie-ticket.com/id=${id}`;

    fetch(url, { method: 'POST' })
      .then((res) => res.json())
      .then((data) => {
        if (!data.isLegal) alert('identified error');
        else {
          orderTicket();
        }
      });
  };

  verifyIdToOrderTicket();
}, [id, name, date, people, theatre, movieId]);

effect 內的 function 宣告盡量不要超過一層

// ❌
useEffect(() => {
  const verifyIdToOrderTicket = () => {
    const verifyIdUrl = `https://www.movie-ticket.com/id=${id}`;

    fetch(verifyIdUrl)
      .then((res) => res.json())
      .then((data) => {
        if (!data.isLegal) alert('identified error');
        else {
          const orderTicket = () => {
            const orderTicketUrl = `https://www.movie-ticket.com/id=${id}&name=${name}&date=${date}&people=${people}&theatre=${theatre}`;

            fetch(orderTicketUrl, { method: 'POST' })
              .then((res) => res.json())
              .then((data) => {
                if (data.success) alert('order successfully');
                else alert('order failed');
              });
          };

          orderTicket();
        }
      });
  };

  verifyIdToOrderTicket();
}, [id, name, date, people, theatre, movieId]);

// ✅ (盡量保持 function 宣告在第一層就好)
useEffect(() => {
  const orderTicket = () => {
    const url = `https://www.movie-ticket.com/id=${id}&name=${name}&date=${date}&people=${people}&theatre=${theatre}`;

    fetch(url, { method: 'POST' })
      .then((res) => res.json())
      .then((data) => {
        if (data.success) alert('order successfully');
        else alert('order failed');
      });
  };

  const verifyIdToOrderTicket = () => {
    const url = `https://www.movie-ticket.com/id=${id}`;

    fetch(url)
      .then((res) => res.json())
      .then((data) => {
        if (!data.isLegal) alert('identified error');
        else {
          orderTicket();
        }
      });
  };

  verifyIdToOrderTicket();
}, [id, name, date, people, theatre, movieId]);

useCallback (記憶「函式」) / useMemo (記憶「值」)

有需要使用再使用

// ❌ 不需要無限擴充,只為了渲染一次
// 大部分渲染的成本,都比使用 useCallback 後的 Component 還要來得低
const App = () => {
  const handleClick = useCallback(() => {
    // do something ...
  }, []);

  const handleOpen = useCallback(() => {
    // do something ...
  }, []);

  const handleClose = useCallback(() => {
    // do something ...
  }, []);

  return (
    <Layout>
      <Button onClick={handleClick}>Click</Button>
      <Button onClick={handleOpen}>Open</Button>
      <Button onClick={handleClose}>Close</Button>
    </Layout>
  );
};

// ✅ 拔除掉 useCallback 後,整份程式碼變得很單純
const App = () => {
  const handleClick = () => {
    // do something ...
  };

  const handleOpen = () => {
    // do something ...
  };

  const handleClose = () => {
    // do something ...
  };

  return (
    <Layout>
      <Button onClick={handleClick}>Click</Button>
      <Button onClick={handleOpen}>Open</Button>
      <Button onClick={handleClose}>Close</Button>
    </Layout>
  );
};

使用時機

  • 畫面確實有明顯效能問題

    • 肉眼可看出卡頓
    • 透過 console.time + console.timeEnd 量測且超過能接受的值 (ex. 1ms))
  • 減少 useEffect dependencies 複雜度

const [datas, setDatas] = useState([]);

useEffect(() => {
  fetch(`https://imdb/get-movie?id=${id}`)
    .then((res) => res.json())
    .then((json) => {
      if (json.success) {
        fetch(`https://imdb/get-movie?people=${people}&name=${name}&date=${date}`)
          .then((res) => res.json())
          .then((json) => {
            setDatas(json);

            fetch(`https://imbd/send-mail?mail=${mail}?content=${content}`)
              .then((res) => res.json())
              .then((json) => {
                if (json.success) {
                  alert('mail sent successfully !!');
                }
              });
          });
      }
    });
  // ❌ dependencies 過多,很難理解觸發條件為何
}, [id, name, date, people, mail, content]);
const sendMail = useCallback(() => {
  fetch(`https://imbd/send-mail?mail=${mail}?content=${content}`)
    .then((res) => res.json())
    .then((json) => {
      if (json.success) {
        alert('mail sent successfully !!');
      }
    });
}, [mail, content]);

const getDatas = useCallback(() => {
  fetch(`https://imdb/get-movie?people=${people}&name=${name}&date=${date}`)
    .then((res) => res.json())
    .then((json) => {
      setDatas(json);
      sendMail();
    });
}, [name, date, people, sendMail]);

const checkId = useCallback(() => {
  fetch(`https://imdb/get-movie?id=${id}`)
    .then((res) => res.json())
    .then((json) => {
      if (json.success) {
        getDatas();
      }
    });
}, [id, getDatas]);

useEffect(() => {
  checkId();
  // ✅ dependencies 變簡單,可以直覺的指到該觸發條件
}, [checkId]);
  • child component 有效能問題
    • 則丟給此 component 的 props 需用 useMemo / useCallback 包裹,且 child component 也需被 React.memo 包裹