Post機能: Front-end

GraphQL APIの実装ができたところで、フロントエンドの実装を行なっていきましょう。

  • 左側のメニュー一覧やPostを表示している部分をSidebarと呼ぶことにします(Sideber.js)
  • 右側のPost一覧を表示する部分は、メニューで選択した機能に応じて以下のコンポーネントを表示します
    • Global Timeline: すべてのユーザーのPostが表示される(AllPost.js)
    • Profile: 特定のユーザーのPostが表示される(PostsBySpecifiedUser.js)
  • どのコンポーネントを表示するかはApp.jsでルーティングします

必要ライブラリのインストール

フロントの構築に必要なライブラリをインストールします。

npm install --save @material-ui/core@4.11.2 @material-ui/icons@4.11.2 moment@2.29.1 react-router@5.2.0 react-router-dom@5.2.0

App.js

次のコードをコピーして、./src/App.jsの中身を書き換えましょう。

  • App.js (2 kb)
  • ポイントは以下です。

    function App() {
      const classes = useStyles();
      return (
        <div className={classes.root} >
          <ThemeProvider theme={theme}>
            <CssBaseline />
            <HashRouter>
              <Switch>
                <Route exact path='/' component={AllPosts} />
                <Route exact path='/global-timeline' component={AllPosts} />
                <Route exact path='/:userId' component={PostsBySpecifiedUser}/>
                <Redirect path="*" to="/" />
              </Switch>
            </HashRouter>
          </ThemeProvider>
        </div>
      );
    }
    
    • react-routerを利用したルーティング処理
      • HashRouter
        • 静的サイト内でブラウザの戻る機能を利用したり、外部から直接特定のコンポーネントにアクセスするため、今回はHashRouterを使用しています
        • https://example.com/#/global-timelineのような形式で、ルーティングが行われます
      • Switch: 配下のRouteのうち、どれか一つにマッチしたらそれのみをレンダリング(Switchなしだとマッチしたもの全てがレンダリングされる)
      • Route
        • /または/global-timelineにアクセスがあった場合、AllPostsコンポーネントをレンダリングして表示
        • /:userId/userAのようなアクセスがあった時にマッチし、PostsBySpecifiedUserコンポーネントをレンダリングして表示し、:userIdをParameterとして受け渡します
          • /global-timelineRouteの上に/:userIdRouteを書いた場合、先に/:userIdとマッチしてしまうため、global-timelineが表示されません
    • ThemeProvider: Material-UIのクラスで、アプリケーション全体のCSSテーマをセット

    Sidebar.js

    Sidebarの役割は主に三つです。

    • Logoutボタンの表示
    • Global TimelineやProfileへの切り替えなど、メニュー機能
    • Postの投稿

    まず、src/containersディレクトリを作り、Sidebar.jsを作成します。

    mkdir src/containers
    touch src/containers/Sidebar.js
    

    以下のSidebar.jsの中身をコピーし、./src/containers/Sidebar.jsを置き換えてください。

    以下で、このコードのポイントについてみていきましょう。

    React Hooksを利用したTextField入力値のハンドリング

    const [value, setValue] = React.useState('');
    
    • useStateHookを使用すると、状態管理が実現できます
    • valueはTextFieldで入力されたPostのcontentを管理します
    • setValuevalueを更新することが可能です
    • useStateを利用して作成されたvaluesetValueにより更新されると、valueを参照するコンポーネントが再レンダリングされます

    本ワークショップではReact HooksやReactの状態管理についての詳しい解説は行いません。 useStateについての詳細はこちらをご覧ください。

    <ListItem key='post-input-field'>
      <ListItemText primary={
        <TextField
          error={isError}
          helperText={helperText}
          id="post-input"
          label="Type your post!"
          multiline
          rowsMax="8"
          variant="filled"
          value={value}
          onChange={handleChange}
          fullWidth
          margin="normal"
        />
      } />
    </ListItem>
    
    • TextFieldの入力値valueに先ほど作成したvalueステートを渡します
    • ユーザーの入力によりTextFieldの入力値が変更された場合、onChange={handleChange}で渡されたhandleChange関数が呼び出されます
    const handleChange = event => {
      setValue(event.target.value);
      if (event.target.value.length > MAX_POST_CONTENT_LENGTH) {
        setIsError(true);
        setHelperText(MAX_POST_CONTENT_LENGTH - event.target.value.length);
      } else {
        setIsError(false);
        setHelperText('');
      }
    };
    
    • handleChangeがTextFieldコンポーネントから呼び出される時、新しい入力値などの情報が引数として渡されます
    • setValue(event.target.value)によって、新しいTextFieldの値をvalueステートに格納しています
    • 同時に140字以下であることを検証しています

    createPost Mutationの実行

    import { createPost } from '../graphql/mutations';
    
    {中略}
      const onPost = async () => {
        const res = await API.graphql(graphqlOperation(createPost, { input: {
          type: 'post',
          content: value,
          timestamp: Math.floor(Date.now() / 1000),
        }})); 
    
        setValue('');
      }
    
    • amplify mock apiamplify pushを実行すると、createPostなどのGraphQL Operationを行うためのコードが./src/graphql配下に書き出されます
    • 書き出されたcreatePostを利用して、Post作成のMutationを実行しているのがonPostメソッドになります
      • 先ほどvalueステートに格納したTextFieldの値を用いています
    • Mutationが成功したらsetValue('')によりTextFieldの値を空文字列にしています

    サインアウト機能の実装

    import Auth from '@aws-amplify/auth';
    
    {中略}
    
      const signOut = () => {
        Auth.signOut()
          .then(data => console.log(data))
          .catch(err => console.log(err));
      }
    
    • サインアウトの実装は、Authモジュールを利用しています。
    • サインアウトが実行されると、サインイン状態でなくなったことをwithAuthenticatorが検知して、サインイン画面に戻ります。

    AllPosts.js

    全てのPostを表示するためのUIを作成していきます。

    touch src/containers/AllPosts.js
    

    以下のAllPosts.jsの中身をコピーし、./src/containers/AllPosts.jsを置き換えてください。

    コードのポイントをみていきましょう。

    全ユーザーのPostを時系列順でフェッチ

    const getPosts = async (type, nextToken = null) => {
      const res = await API.graphql(graphqlOperation(listPostsSortedByTimestamp, {
        type: "post",
        sortDirection: 'DESC',
        limit: 20, //default = 10
        nextToken: nextToken,
      }));
      console.log(res);
      dispatch({ type: type, posts: res.data.listPostsSortedByTimestamp.items })
      setNextToken(res.data.listPostsSortedByTimestamp.nextToken);
      setIsLoading(false);
    }
    
    • 3.2.で作成したlistPostsSortedByTimestampを用いて、全てのユーザーのPostをtimestampの降順でリストアップしています
    • limitで取得件数を制御することができます。今回は20件取得しており、デフォルトは10件です。
    • nextTokenは取得したデータの次の20件がある場合にセットされるトークンです。NextTokenを指定するすることにより、timestampの降順で20件ずつフェッチしてくることが可能です

    Subscription: リアルタイムでの新規Postの取得

    useEffect(() => {
      getPosts(INITIAL_QUERY);
    
      const subscription = API.graphql(graphqlOperation(onCreatePost)).subscribe({
        next: (msg) => {
          console.log('allposts subscription fired')
          const post = msg.value.data.onCreatePost;
          dispatch({ type: SUBSCRIPTION, post: post });
        }
      });
      return () => subscription.unsubscribe();
    }, []);
    
    • useEffectHookを利用して、ComponentのMountが完了した後の処理を記述しています(useEffectに関しての詳細はこちら)
    • Subscriptionを発行しており、createPost Mutationが呼ばれるたびに、.subscribeに渡したArrow Functionが実行されます
    • unsubscribeするためのArrow FunctionをReturnすることにより、ComponentがUnmountされた際にSubscriptionを解除しています

    PostsBySpecifiedUser.js

    特定のユーザーのPost一覧を表示するためのUIを作成していきます。

    touch src/containers/PostsBySpecifiedUser.js
    

    以下のPostsBySpecifiedUser.jsの中身をコピーし、./src/containers/PostsBySpecifiedUser.jsを置き換えてください。

    const getPosts = async (type, nextToken = null) => {
      const res = await API.graphql(graphqlOperation(listPostsBySpecificOwner, {
        owner: userId,
        sortDirection: 'DESC',
        limit: 20,
        nextToken: nextToken,
      }));
    
      dispatch({ type: type, posts: res.data.listPostsBySpecificOwner.items })
      setNextToken(res.data.listPostsBySpecificOwner.nextToken);
      setIsLoading(false);
    }
    
    • listPostsBySpecificOwnerを使用して、ownerにURIパラメータからとってきたuserIdをセットしています

    PostList.js

    AllPosts.jsPostsBySpecifiedUser.jsから渡されたPost一覧を表示するためのUIを作成していきます。

    mkdir src/components
    touch src/components/PostList.js
    

    以下のPostList.jsの中身をコピーし、./src/components/PostList.jsを置き換えてください。

    本ワークショップではuseStateを使用して状態管理を行なっているコンポーネントは./src/containers、そうでないコンポーネントは./src/componentsに配置しています。

    ファイル構成の確認

    treeコマンドで現在のディレクトリ構造を確認すると以下のようになっています。 環境によってはtreeコマンドがデフォルトではインストールされていないことがあります。 その場合別途インストールいただくか、他の手段でディレクトリ構造をご確認ください。

    tree src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── aws-exports.js
    ├── components
    │   └── PostList.js
    ├── containers
    │   ├── AllPosts.js
    │   ├── PostsBySpecifiedUser.js
    │   └── Sidebar.js
    ├── graphql
    │   ├── mutations.js
    │   ├── queries.js
    │   └── subscriptions.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    ├── reportWebVitals.js
    └── setupTests.js
    

    ポイント

    • componentsの配下にPostList.jsがあります
    • containersの配下にはAllPosts.jsPostsBySpecifiedUser.jsSidebar.jsがあります

    動作確認

    ここまでの変更をクラウドに反映して確認していきましょう。(実行には数分かかります)

    amplify push
    

    もし、$ amplify mock apiが動いているようであれば、Ctrl+Cでコマンドの実行を止めておいてください。

    $ amplify mock api$ npm startを同時に実行し、$ amplify pushせずに動作確認を行うことも可能です。ただし、作業環境にコンテナをお使いの場合はaws-exports.jsファイルのキーが"aws_appsync_graphqlEndpoint"の値を"http://localhost:20002/graphql"に変更する必要があることにご注意ください。

    それでは、UIの実装がうまくいっているか確認しましょう。 $ npm startが実行されていない場合は、改めて実行しておきます。

    • Postが動くことを確認します
    • LOGOUTできることを確認します
    • 複数ウェブブラウザを開き、別のユーザーでログインしてPostしてみましょう(同じメールアドレスであっても、Usernameを変えれば複数のユーザーを作成することができます)
    • Profileを押すと自分のPostだけが表示されます