20200806のReactに関する記事は8件です。

【第0回】「みんなのポートフォリオまとめサイト」を作ります~宣言編~

きっかけ

「アウトプットしたいけど、何作ればいいんや?」
「オレの今の技術だと、どんなものが作れるんや?」

プログラミング学習中こんな風に思って困ったこと、ありませんか?

僕はめちゃくちゃありました!

Twitterを見てるとたまーにTLに誰かのアウトプット作品が流れてきたりしますが、それもちょうど今の自分のレベルに合ったものでないとなかなか参考にしづらい。

じゃあQiitaはどうなんだ!?ということで、Qiitaでたとえば「JavaScript アプリ」とか「JavaScript ゲーム」などのキーワードで検索しても、ソースコードしか載ってないパターンがほとんどで、あってもスクショ画像が1枚だけ。
せめて動かしてるデモのgif画像くらい見てみたいですし、実際にサーバーにアップしているURLが記事内にあって、実物を触れるパターンなんて全然ないなと感じていました。

しかし、プログラミングを始めて1年半ほど経ったので今なら分かりますが、世の中には自分の成果物をネット上のどこかで公開している人がたくさんいます!

だってみなさん、ポートフォリオを持って転職活動しますでしょ?

てことは、企業に見て触ってもらうために、実際に公開する人が多いと思うんです(ローカルでしか動きません、だとなかなかアピールしづらいですしね

でも、ネット上に公開している人がたくさんいるはずなのに、「ここにいけばみんなのポートフォリオが一度にたくさん見れる!わーい!」みたいな場所ってない気がするんです(あったら教えてください)

じゃあ、自分で作っちゃおう!というのが今回の趣旨でございます。

他人の成果物を見てみたい人自分の成果物を他人に見てもらいたい人
双方にメリットがあるようなプラットフォームにしていきたいです。

1からサービス作ったことがない人にとっては、どういう手順で作っていくのかをリアルタイムで晒しているというのは多少なりとも参考になる人もいるのでは?と思い、「作ってみた」じゃなくて「これから作ります」というスタンスの記事にしてみました。

そして、作る過程を晒すことで、作っている最中からみなさんにアドバイスをいただいてよりよいサービスにしていきたいという作戦であります!(ここ重要!)

どうぞよろしく!

自己紹介

新卒で入った食品メーカーにて、チャーハン職人として製造現場でチャーハンを作りまくるみたいな仕事を6年くらいやって退職。
2019年4月よりプログラミング学習を開始。2019年10月よりフロントエンドエンジニアとして都内で働いております。今は某事業会社のいわゆる大規模開発といわれるような現場で開発をしています。

今回は、「React,Reduxでなんか作る」そして、「アクティブユーザーのいるサービスを作る」という2つの目的を達成すべく、サービス作りを進めていきます。

対象ユーザー

  • 他の人の成果物を見てみたい人
  • 自分の成果物を他の人に見てもらいたい人

がんばりたいポイント

このサービス、まず成果物を公開してくれるユーザーがいないと始まらないんです。

なので、 集客という点が非常に重要になってきます。

今後自分が事業を何かやるにしても必ず集客は必要になってきます。その第一歩としてこのサービスのユーザーを増やしていきたい。

「何かに役立つはず…」と思いTwitterでの発信もコツコツ続けてきました。おかげさまでフォロワーも結構増えました。何かに役立てるとしたら、今ここだろうという気がします。

今の自分が本気で集客をがんばったらどれくらいのことができるのか、そんな自分への挑戦でもあります。

使用技術

  • フロントエンド・・・React, Reduxを使う(マスト)、react-routerを使ってSPA(風)にする(できれば)
  • バックエンド・・・Laravel

機能

やってくうちに増減あると思いますが、今んとここんな感じの予定。
まずは機能最小限で完成優先。

  • ユーザー登録(1アカウントで成果物の投稿・閲覧両方可能)
  • ログイン(SNS認証あり)
  • ログアウト
  • プロフィール編集
  • ユーザー削除
  • コンテンツ(成果物)登録
  • コンテンツ(成果物)編集/更新
  • コンテンツ(成果物)削除
  • いいね
  • フォロー

工数見積

1人月(160時間)
できるかなーどうかなー

おわりに

これぞまさに 宣言駆動開発 !!!
先にやるって言っちゃうとやるしかなくなるので、おすすめです。
今後は1週間ごとくらいで進捗報告していこうかと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactで入力候補機能を実装

概要

Reactの勉強としてwebアプリを作る中で、以下のようなテキスト入力中のサジェスト機能をカスタムフック「useSuggestion」として実装したので実装方法を記載します。
GIF 2020-08-06 9-53-38.gif

開発環境

"react": "^16.13.1"
"typescript": "^3.9.7"

使い方

引数

  1. 文字列
  2. 単語リスト
  3. 入力候補を選択した時に実行されるコールバック関数
  4. 単語検索用のフィルター関数(省略可能)

引数に文字列、単語リスト、入力候補を選択した時に実行されるコールバック関数を渡します。
デフォルトでは単語リストとして文字列が入った配列を渡す必要がありますが、自作したフィルター関数を第4引数に渡すことでオブジェクトが入った配列を扱うことも出来ます。

返り値

  • suggestions: 入力候補のリスト
  • activeIndex: 現在選択中の入力候補のインデックス(初期値は-1)

返り値の入力候補リストをulタグなどで表示すればOKです。
矢印キーやEnterキーを押したときの処理はuseSuggestion内に含まれています。

使用例SuggestionList
interface SuggestionListProps {
  text: string;
  words: string[];
  callbackSelectSuggestion: any;
}

const SuggestionList: React.FC<SuggestionListProps> = ({
  text,
  words,
  callbackSelectSuggestion
}) => {
  const { suggestions, activeIndex } = useSuggestion(text, words, callbackSelectSuggestion);

  return (
    <>
    { 
      (suggestions.length > 0) && 
      <ul className="suggestion-list">
      {
        suggestions.map((suggestion, i) => {
          const isActive = i === activeIndex ? 'is-active' : '';
          return  (
            <li 
              key={suggestion} 
              className={isActive} 
              onClick={()=>{ callbackSelectSuggestion(suggestion) }}
            >
              {suggestion}
            </li>
          )
        })
      }
      </ul>
    }
    </>
  )
}

export default SuggestionList;

実装内容

useSuggestion

useSuggestionのコードは以下のようになります。
主に入力候補の取得とKeydownイベントの処理の登録を行っています。
デフォルトのフィルター関数には後述するgetWordsContainingStringを使用しています。その他、keydownイベント用関数を生成するヘルパー関数としてcreateKeydownHandlerを読み込んでいます。

useSuggestion.ts
import { useMemo, useState, useCallback, useEffect } from 'react';
import getWordsContainingString from './getWordsContainingString';
import createKeydownHandler from './createKeydownHandler';

interface UseSuggestion {
  (
    str: string,
    list: any[],
    callbackSelectSuggestion: any,
    filter?: (str: string, list: any[]) => any[]
  ) : {
    suggestions: any[];
    activeIndex: number;
  };
}

export const useSuggestion: UseSuggestion = (str, list, callbackSelectSuggestion, filter = getWordsContainingString) => {
  const [activeIndex, setActiveIndex] = useState<number>(-1);

  const suggestions = useMemo(() => {
    return filter(str, list);
  }, [str, list]);

  // 上矢印キーを押したときに選択中のインデックスを変更する関数
  const handleKeydownArrowUp = useCallback(createKeydownHandler({
    key: 'ArrowUp',
    control: false,
    handler: (ev) => {
      if (suggestions.length === 0) return;
      ev.preventDefault();
      const newIndex = activeIndex - 1 >= -1 ? activeIndex -1 : suggestions.length - 1; 
      setActiveIndex(newIndex);
    }
  }), [suggestions, activeIndex, setActiveIndex]);

  // 下矢印キーを押したときに選択中のインデックスを変更する関数
  const handleKeydownArrowDown = useCallback(createKeydownHandler({
    key: 'ArrowDown',
    control: false,
    handler: (ev) => {
      if (suggestions.length === 0) return;
      ev.preventDefault();
      const newIndex = activeIndex + 1 < suggestions.length ? activeIndex + 1 : -1; 
      setActiveIndex(newIndex);
    }
  }), [suggestions, activeIndex, setActiveIndex]);

  // エンターを押した時に引数で渡されたコールバック関数を実行する処理
  const handleKeydownEnter = useCallback(createKeydownHandler({
    key: 'Enter',
    control: false,
    handler: (ev) => {
      if (activeIndex === -1 || suggestions.length === 0) return;
      ev.preventDefault();
      callbackSelectSuggestion(suggestions[activeIndex]);
    }
  }), [suggestions, callbackSelectSuggestion, activeIndex]);

  // keydownイベント登録関数を一つにまとめる
  const handleKeydown = useCallback((ev:  KeyboardEvent) => {
    handleKeydownArrowUp(ev);
    handleKeydownArrowDown(ev);
    handleKeydownEnter(ev);
  }, [handleKeydownArrowUp, handleKeydownArrowDown, handleKeydownEnter]);

  // keydownイベント登録と解除
  useEffect(() => {
    window.addEventListener('keydown', handleKeydown);
    return () => { window.removeEventListener('keydown', handleKeydown)};
  }, [handleKeydown]);

  return { suggestions, activeIndex };
}

export default useSuggestion

文字列から入力候補を検索するフィルター関数
getWordsContainingString

検索する文字列と単語リストを引数に受け取り、検索結果を配列で返す関数です。
ひらがな、カタカナ関係なく検索できるように一文字ずつひらがなとカタカナに変換して正規表現に加えています。また、ローマ字の小文字と大文字も同様に変換して正規表現に加えています。

getWordsContainingString.ts
interface Converter {
    (str: string): string;
}

const kana2hira = (str:string): string => {
    return str.replace(/[\u30a1-\u30f6]/g, (match: string):string => {
        const chr = match.charCodeAt(0) - 0x60;
        return String.fromCharCode(chr);
    })
}

const hira2kana = (str: string): string => {
    return str.replace(/[\u3041-\u3096]/g, (match: string) => {
        const chr = match.charCodeAt(0) + 0x60;
        return String.fromCharCode(chr);
    })
}

const lower2upper = (str: string): string => {
    return str.toUpperCase();
}

const upper2lower = (str: string): string => {
    return str.toLowerCase();
}

// 検索文字列から正規表現を作成する関数。
// ひらがな・カタカナ対しても小文字・大文字への変換をしている(ひらがな・カタカナのまま)ので、
// 一文字に対して4パターン作成されます。
// 例)「あ」の場合、
// ひらがな変換 →「あ」 カタカナ変換 → 「ア」 小文字変換 →「あ」 大文字変換 →「あ」
// あい -> ^[あアああ][いイいい].$ 
// abc  -> ^[aaaA][bbbB][cccC].$

const createSearchPattern = (str: string, converters: Converter[]):string => {
    let convertedWords:string[] = converters.map((converter):string => {
        return converter(str);
    });
    let pattern:string = '^';
    for (let i = 0; i < str.length; i++) {
        pattern += '[';
        for (let j = 0; j < convertedWords.length; j++){
            pattern +=  convertedWords[j][i];
        }
        pattern += ']';
    }
    pattern += '.*$';
    return pattern;
}

const getWordsContainingString = (str: string, words: string[]): string[] => {
    if (str === '') return [];
    const converters = [hira2kana, kana2hira, upper2lower, lower2upper];
    const pattern = createSearchPattern(str, converters);
    const reg = new RegExp(pattern);
    const hits = words.filter(word => {
        return word.match(reg);
    });

    return hits;
}
export default getWordsContainingString;

イベント用関数を生成するヘルパー関数
createKeydownHandler

引数にキーボード押下時のキー(Shift、Enterなど)、登録する関数、Ctrlキー押下を判定に加えるかの設定を渡すことで、
任意のキー押下時のみ処理される関数を作ることができます。

createKeydownHandler.ts
interface Option {
  key: string;
  handler: (ev: KeyboardEvent) => unknown;
  control?: boolean;
}

const createKeydownHandler = (option: Option) => {
  return (ev: KeyboardEvent): any =>  {
    if (ev.key === option.key  && ev.ctrlKey === !!option.control) {
      return option.handler(ev);
    }
  }
}

export default createKeydownHandler;

おわりに

最後までご覧いただきありがとうございます。
なるべく再利用可能なコードになるように心がけましたが、
React、typescriptはまだまだ実務未経験のレベルなので、もっとこうしたほうがいいなどあればコメントいただければ幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.memoとuseCallbackを使用したコンポーネントのパフォーマンス向上について。

今、社内のプロジェクトでシステムを開発していてフロントエンドをやらせてもらってるんですが
その中でReact Hooksを勉強しています。
ということでその中で使用したuseCallbackと、HooksではありませんがReact.memoに関して
初学者の方でも分かっていただけるように書いていければと思います。

Hookに関して

Hooksなん?Hookなん?
とか少し思ってたんですが、これは複数形ですね。
いくつかHookがあるんで、Hooksと呼ぶんだと思います。
一旦公式を。フックの導入

なんでも、Reactが開発されてから色んな問題に直面することがあったらしく、それらを解決するために導入されたようです。
Hookがなんたるか、知ることも大切ですが今回はその中でもuseCallbackに関して書いていくのでここでは詳細は省きます。公式をご覧ください。

useCallbackを使用すると何が良いの?

Reactは本来ストアされている情報をレンダリングするために作られたフレームワークです。
コンポーネントなどではストアの情報に変更があった際にその変更に伴って再レンダリングが発生して
描画しなおしています。

ここで、問題になるのは関数などはどうなるのか?
以下のような状態を考えます。
ちなみに、Material UIのサンプルコードです。

export default function SimpleSnackbar() {
  const [open, setOpen] = React.useState(false);

  const handleClick = () => {
    setOpen(true);
  };

  const handleClose = (event, reason) => {
    if (reason === 'clickaway') {
      return;
    }

    setOpen(false);
  };

  return (
    <div>
      <Button onClick={handleClick}>Open simple snackbar</Button>
      <Snackbar
        anchorOrigin={{
          vertical: 'bottom',
          horizontal: 'left',
        }}
        open={open}
        autoHideDuration={6000}
        onClose={handleClose}
        message="Note archived"
        action={
          <React.Fragment>
            <Button color="secondary" size="small" onClick={handleClose}>
              UNDO
            </Button>
            <IconButton size="small" aria-label="close" color="inherit" onClick={handleClose}>
              <CloseIcon fontSize="small" />
            </IconButton>
          </React.Fragment>
        }
      />
    </div>
  );
}

先ほども言ったように、ストアされている情報に変更があった際にはこのコンポーネントには再レンダリングがかかります。
すると、コンポーネント内部で定義されているhandleCloseなども再定義されて、関数が作成されてしまうことになります。
ですが、あくまでここで使われているhandleClose自体の引数に変化があったりしたかと言われるとそうではないため、ここで再び作成されるhandleCloseは最初にレンダリングされた際に定義されたものと全く同じものになります。
このように、レンダリングされるたびに同じ関数が何度も何度も作成されてしまうのです。

これでは無駄すぎるから、引数に変化があったりしたときだけ関数を定義し直して欲しい!!!
ここで使用されるのがuseCallbackです。
どういった仕組みでレンダリングの際に定義されることを防いでいるか詳細まではわかりませんが、
感覚的には一旦退避場所に逃しておく感じ。
こうすることで、コンポーネントは無駄な関数を再定義して作成し続ける仕事をしなくてよくなるので
パフォーマンスは向上されます。

じゃあ、React.memoは?

では、useCallbackがどんなことをしてくれるかなんとなく分かったところで
同じことがコンポーネントにも言えるのでは?と考えられます。

ここでも簡単な例をあげます。

index.js
const Index = () => {
  return (
    <Header />
    <MainSection />
    <Footer />
  );
};

export default Index;

こんな感じで、あるウェブサイトのトップページが記載されているとします。
このような場合ヘッダーやフッターは基本的に固定の内容が記載されるはずなので、
ストアの情報が変更されるときに、再レンダリングがかかるべきはMainSectionのコンポーネントのみということになります。
ですが、普通に書いてしまうと全部が再レンダリングされてしまう。
ここでReact.memoを使います。
こちらも公式のドキュメントのリンクを貼っておきます。React.memo

const Header = React.memo(function Header(props) {
  /* render using props */
});

const Footer = React.memo(function Footer(props) {
  /* render using props */
});

このようにHeaderとFooterを定義してあげると、同じpropsを与えられたときに同じ結果をレンダーするなら、結果を記憶してくれます。コンポーネントのレンダーはスキップされて、最後のレンダー結果を再利用するようになります。
そのため、無駄なくレンダリングできるようになるのでパフォーマンスが向上するというわけです。

適切に使用して良いコンポーネントを作成しましょう

このように、パフォーマンスを向上させるためにuseCallbackReact.memoを利用してコンポーネントの作成を行いました。
今後はもっと勉強してより質の高いコードをかけるようにしていければと思います。

また、ここで書いているのは僕の備忘録なので表現だったり理解が誤っている可能性がないとは言えません。
あくまで、そういう物があるんだなくらいに初学者の方々には読んでいただけるとありがたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでデバッグに使う用のテキストエディタを作った

やったこと

Reactで作ったコンポーネントの動作確認をするのに、渡すpropsを直接ソースに打ち込んでコンパイルして確認して・・・という流れを毎回辿るのは、ホットリロードがあっても辛かった。

何もプロジェクト全体をビルドしなくても、そのコンポーネントに渡すpropsだけを差し替えられれば良いのだから、画面上に置いたtextareaにJSONを書いて渡せば良いんじゃないかと思ってやってみた。

DebugTextEditor.tsx

使い方

例えばこんな感じ。
onChangeイベントでhandleUpdateDebugDataを発火して、エディタに書かれた内容をJSONにパースして、テストしたいComponentのpropsに渡している。
JSONのパースに失敗したときはエラーの内容がエディタの方に表示される。

使用例
import React, { useState, useCallback } from 'react'
import { DebugTextEditor } from './DebugTextEditor'

const Index = props => {
    const [data, setData] = useState({})
    const [error, setError] = useState("")
    const handleUpdateDebugData = useCallback(str => {
        try {
            setData(JSON.parse(str))
            setError("")
        }
        catch (e) {
            setError([e.name, e.message].join(":"))
        }
    }, [setData, setError])
    return (<>
        <YourComponent data={data} />
        <DebugTextEditor onChange={handleUpdateDebugData} error={error} />
    </>)
}
その他オプション
<DebugTextEditor
    // valueで初期値を設定可能
    value=""
    // onSubmitを指定するとSubmitボタンが現れる
    onSubmit={handleUpdateDebugData}
    // 各要素のstyleをprops.styleから上書き可能
    style={
        // 親要素(form)のスタイル
        ...
        textarea: {
            // textarea要素のスタイル
            ...
        },
        error: {
            // error用要素のスタイル
            ...
        },
        button: {
            // button要素のスタイル
            ...
        }
    } 
/>

今後やりたいこと

  • エディタ部分は素のtextareaまんまなので、Tabキーが使えなかったり自動インデントできなかったりでJSON文字列を書くには辛い
    • Editor.jsとかDraft.jsとか使えば良いのかなとも思うけど手を出せてない。やりたいことに対して手段がデカすぎる気もしている。
  • npmで公開しないと、正直、自分がこの記事を読んだとして、使ってみようとは思わない。
    • 単にやったことないからだろうけどハードルの高さを感じている。うーん。
  • Productionビルド時に勝手に消えてくれたらいいなあ
  • ページをリロードする度に打ち込んだ文字列が消えるので、LocalStorageに保存しておきたい
  • styleの指定方法が小慣れてないので、なんか良い方法あったら知りたい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nextjsで複数のレイアウトをサーバー側(SSR)で特定し出力したい

useStateを使う問題

https://nextjs.org/docs/advanced-features/custom-app
ページ全体に共通なレイアウトはcustom-appを使って設定できる、がuseStateなんかをして動的に変更すると一瞬DefaultLayoutが表示されて、その後にLayout2に切り替わるようなことになってしまう。

雑な例
import type { AppProps } from 'next/app'
...
...

function MyApp(props: AppProps) {
  const { Component, pageProps } = props
  const [layoutType, ...] = useState()
  ...
  ...

  const Layout = layoutType == ... ? Layout2 : DefaultLayout

  return(
    <>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </>
  )
}

export default MyApp

解決方法

AppPropsのrouter.routeを使う。

function MyApp(props: AppProps) {
  const { Component, pageProps, router: { route } } = props

  console.log("route", route) // => pages直下のパスが返ってくる  

このrouteのパスで、パス毎で表示に必要なLayoutを切り替えてやればクライアント側で動的に変わらない。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[メモ] React.FC の PropTypes 指定

[メモ] React.FC の PropTypes 指定

React.FC で PropTypes を使用する場合に以下の記述にする

react-prop-types
import PropTypes from 'prop-types';
import React from 'react';

/** Props の型を定義 */
type Props = {
  /** こども */
  children?: React.ReactNode;
};

/**
 * Component.propTypes に指定する変数
 * - 上記の Props を `React.WeakValidationMap` に渡す
 */
const propTypes: React.WeakValidationMap<Props> = {
  children: PropTypes.node,
};

const Component: React.FC<Props> = (props) => <div>{props.children}</div>;

Component.propTypes = propTypes;
export default Component;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactの状態管理はRecoilがトップに! Reduxは2位に転落!!

グーグルトレンドで遊んでいると、RecilがReduxを抜いてトップに躍り出ていました、差は僅かですがこれは大きな出来事だと思います。

Redux, recoil, mobx, useContext - 調べる - Google トレンド

Recoil Redux.PNG

ただし、日本国内ではReduxが難攻不落の要塞となっています。

Recoilについては
Reactの実験的ステート管理ライブラリRecoilの基本的な使い方
この記事がわかりやすかったです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Apollo ClientからApollo Serverへファイルのアップロード

はじめに

Apollo ClientからApollo ServerにGraphQLでファイルをアップロードするにあたり、必要だった知識をメモしておきます。なお、ファイルのダウンロードに関しては、base64形式等の文字列をフロントに返すといったやり方もあるようですが、素直にRESTのエンドポイントで返す形にしました。

Apollo Client 側

apollo-upload-clientというミドルウェアをApolloLinkに追加(この記事を書いた時点ではApollo Clientとの型の整合性が取れておらず、 //@ts-ignore する必要がありました)

const client = new ApolloClient({
  cache: new InMemoryCache(),
  //@ts-ignore
  link: createUploadLink({
    uri: "http://localhost:4000",
  }),
});

onChangeイベントで

<input type="file" onChange={onChange}>

mutationリクエスト

const UPLOAD_FILE = gql`
  mutation($file: Upload!) {
    uploadFile(file: $file) {
        success
    }
  }
`;

Apollo Server 側

第2引数の file に、アップロードされたファイルに関するオブジェクトが渡ってくるので、その中のcreateReadStreamでファイルのStreamオブジェクトを取得します。その後、aws-sdkを使ってS3にそのまま流し込んだり、CSVのファイルをjson形式に展開するのであればcsvtojsonを使うなど、必要な処理に進みます。

async uploadFileResolver (
  parent, 
  { file } : { file: Promise<FileUpload> }
) : Promise<UploadFileResponse> {
  const { createReadStream, filename, mimetype, encoding } = await file;
  // createReadStream()でファイルのStreamが取得できる
}

内部で使われているgraphql-uploadFileUploadの型がこちら

export interface FileUpload {
  filename: string;
  mimetype: string;
  encoding: string;
  createReadStream(): ReadStream;
}

ファイルのmaxサイズを指定したい場合は

const server = new ApolloServer({
  typeDefs,
  resolvers,
  uploads: {
    maxFileSize: 10000,
  },
});

Codegen

Apollo Serverが自動的に追加してくれるので、スキーマファイルに scalar Upload を記述する必要はないのですが、graphql-code-generatorを使っている場合は設定ファイルに scalar Upload を追加する必要があります。

https://github.com/dotansimha/graphql-code-generator/issues/1073#issuecomment-448685684

まとめ

今回はApollo ClientからApollo Serverへファイルのアップロードをする際に必要だった知識をまとめました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む