【ReactJS】絶対に使いたいライブラリ8選!
2099-12-31

ReactJS はエコシステムも優秀です。

この記事では React に付随する ライブラリの 簡単なデモ と 実装例を紹介します。

備考

  • react-redux とか react-router-dom とかそういう超有名なライブラリは今回は取り扱いません。というかとてもじゃないけど説明しきれません。
  • バージョンは 執筆時点の最新を使います。
  • バージョンによってオプションとかは変わったりするので、あくまで参考程度に御覧ください。
  • 詳しいオプションとかはリファレンスを参照してください。
目次

Settings

ビルドに使用した設定ファイルは以下です。

package.json

{
  "name": "react-form-libraries",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "dependencies": {
    "react": "^16.5.2",
    "react-autocomplete": "^1.8.1",
    "react-dnd": "^5.0.0",
    "react-dom": "^16.5.2",
    "react-draggable": "^3.0.5",
    "react-dropzone": "^5.1.0",
    "react-modal": "^3.5.1",
    "react-notification-system": "^0.2.17",
    "react-pdf": "^3.0.5",
    "react-select": "^2.0.0"
  },
  "devDependencies": {
    "babel": "^6.23.0",
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.5",
    "babel-plugin-transform-decorators": "^6.24.1",
    "babel-plugin-transform-object-rest-spread": "^6.26.0",
    "babel-polyfill": "^6.26.0",
    "babel-preset-react": "^6.24.1",
    "stylus": "^0.54.5",
    "stylus-loader": "^3.0.2",
    "webpack": "^4.19.1",
    "webpack-cli": "^3.1.0"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
webpack.config.js

const path = require("path");
const relpath = __dirname.split('/').splice(-2, 2).join('/')

module.exports = {
  context: __dirname,
  mode: "development",
  devtool : 'source-map',
  entry: [
    path.resolve("./entry.js")
  ],
  output: {
    path: path.resolve('.'),
    filename: "bundle.js",
    publicPath: './',
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.styl$/,
        use: ["style-loader", "css-loader", "stylus-loader"],
      },
      {
        test: /\.js?$/, 
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['react'],
              compact: false,
            },
          }
        ]
      }
    ]
  },
  resolve: {
    extensions: [".js", ".json", ".styl"],
    modules: ['./node_modules'],
    alias: {
      '@top': path.join(__dirname, '.'),
      '@components': path.join(__dirname, './components'),
      '@styles': path.join(__dirname, './styles'),
    },
  },
  plugins: [],
  watchOptions: {
    poll: 1000,
  },
};
entry.js

import React from 'react'
import ReactDOM from 'react-dom'

import NotificationComponent from '@components/react-notification-system'
import PdfComponent from '@components/react-pdf'
import ModalComponent from '@components/react-modal'
import DndComponent from '@components/react-dnd'
import DraggableComponent from '@components/react-draggable'
import SelectComponent from '@components/react-select'
import DropzoneComponent from '@components/react-dropzone'
import AutoCompleteComponent from '@components/react-autocomplete'

import '@styles/common.styl'

ReactDOM.render(<NotificationComponent />, document.querySelector('#demo-react-notification-system'))
ReactDOM.render(<PdfComponent />, document.querySelector('#demo-react-pdf'))
ReactDOM.render(<ModalComponent />, document.querySelector('#demo-react-modal'))
ReactDOM.render(<DndComponent />, document.querySelector('#demo-react-dnd'))
ReactDOM.render(<DraggableComponent />, document.querySelector('#demo-react-draggable'))
ReactDOM.render(<SelectComponent />, document.querySelector('#demo-react-select'))
ReactDOM.render(<DropzoneComponent />, document.querySelector('#demo-react-dropzone'))
ReactDOM.render(<AutoCompleteComponent />, document.querySelector('#demo-react-autocomplete'))
.babelrc

{
  "presets": ["env", "react", ["env", {
    "targets": {
      "node": "current"
    }}]
  ],
  "plugins": [
    "transform-object-rest-spread",
    "transform-decorators"
  ]
}

react-notification-system

通知用のライブラリです。

バージョン
0.2.17
リポジトリ
https://github.com/igorprado/react-notification-system
デモ
コード
import React from 'react'
import NotificationSystem from 'react-notification-system'



export default class Index extends React.Component {
  constructor (props) {
    super(props)
    this.state = {level: 'success', position: 'tr', uid: 0}
  }
  addNotification (notification) {
    this.refs.notificationSystem.addNotification({
      ... notification,
      action: {
        label: 'Follow me',
        callback: () => window.open('https://twitter.com/crohaco'),
      },
    })
    this.setState({uid: this.state.uid + 1})
  }
  render () {
    return <div className="component">
      <fieldset>

        <div>Title:
          <input value={this.state.title || ''} onChange={e => this.setState({title: e.target.value})} />
        </div>

        <div>Message:
          <input value={this.state.message || ''} onChange={e => this.setState({message: e.target.value})} />
        </div>

        <div>Level:
          &nbsp;<label>成功:<input type="radio" checked={this.state.level === 'success'} onChange={e => this.setState({level: 'success'})} /></label>
          &nbsp;<label>エラー:<input type="radio" checked={this.state.level === 'error'} onChange={e => this.setState({level: 'error'})} /></label>
          &nbsp;<label>警告:<input type="radio" checked={this.state.level === 'warning'} onChange={e => this.setState({level: 'warning'})} /></label>
          &nbsp;<label>情報:<input type="radio" checked={this.state.level === 'info'} onChange={e => this.setState({level: 'info'})} /></label>
        </div>

        <div>Position:
          &nbsp;<label>左上:<input type="radio" checked={this.state.position === 'tl'} onChange={e => this.setState({position: 'tl'})} /></label>
          &nbsp;<label>中上:<input type="radio" checked={this.state.position === 'tc'} onChange={e => this.setState({position: 'tc'})} /></label>
          &nbsp;<label>右上:<input type="radio" checked={this.state.position === 'tr'} onChange={e => this.setState({position: 'tr'})} /></label>
          &nbsp;<label>左下:<input type="radio" checked={this.state.position === 'bl'} onChange={e => this.setState({position: 'bl'})} /></label>
          &nbsp;<label>中上:<input type="radio" checked={this.state.position === 'bc'} onChange={e => this.setState({position: 'bc'})} /></label>
          &nbsp;<label>右下:<input type="radio" checked={this.state.position === 'br'} onChange={e => this.setState({position: 'br'})} /></label>
        </div>

        <div>Dismiss:
          <input 
            placeholder="何秒で消す?0で無限"
            type="number" value={this.state.autoDismiss || 0} 
            onChange={e => this.setState({autoDismiss: parseInt(e.target.value || 0)})} 
          /> second(s) later, the notification will disappear.
        </div>

        <button onClick={() => this.addNotification(this.state)}>Add notification</button>
      </fieldset>
      <NotificationSystem ref="notificationSystem" />
    </div>
  }
}

react-pdf

バージョン
3.0.5
リポジトリ
https://github.com/wojtekmaj/react-pdf
デモ
コード
import React from 'react'
import { Document, Page } from 'react-pdf';


export default class Index extends React.Component {
  constructor (props) {
    super(props)
    this.state = {pdf: null, page: 1}
  }

  onDocumentLoad ({ numPages }) {
    this.setState({numPages})
  }

  render () {
    return <div className="component">
      PDFを添付してください:
      <input type="file" onChange={e => this.setState({pdf: e.target.value})}
      />
      <Document 
        file={this.state.pdf} style={{border: 'dotted 1px #aaa'}}
        onLoadSuccess={this.onDocumentLoad.bind(this)}
      >
        <Page 
          pageNumber={this.state.page} 
        />
      </Document>
      <button disabled={this.state.page <= 1}>Prev</button>
      {this.state.page || 0} / {this.state.numPages || 0}
      <button disabled={this.state.page >= this.state.numPages}>Next</button>
      
    </div>
  }
}

PDF.js を生で操作すると pdfjs-dist/build/pdfpdfjs-dist/web/pdf_viewer の読み込みを別途行わないといけないし、 特定の環境だけ worker.js が読み込まれなくて落ちたりして辛い

react-modal

バージョン
3.5.1
リポジトリ
https://github.com/reactjs/react-modal
デモ
コード
import React from 'react'
import Modal from 'react-modal'

import '@styles/react-modal.styl'

export default class Index extends React.Component {
  constructor (props) {
    super(props)
    this.state = {open: false}
  }

  handleClose () {
    this.setState({open: false})
  }

  render () {
    return <div className="component">
        <button
          onClick={() => this.setState({
            open: true, code: 'cGlFxkWgI84',
            shouldCloseOnOverlayClick: true,
            shouldCloseOnEsc: false,
            closeButton: null,
          })}
        >1</button>

        <button
          onClick={() => this.setState({
            open: true, code: 'kfqToOh7MVA',
            shouldCloseOnOverlayClick: false,
            shouldCloseOnEsc: true,
            closeButton: null,
          })}
        >2</button>

        <button
          onClick={() => this.setState({
            open: true, code: 'H4znsXCH_2Y',
            shouldCloseOnOverlayClick: false,
            shouldCloseOnEsc: false,
            closeButton: <button 
              onClick={this.handleClose.bind(this)}
              style={{backgroundColor: '#f00', position: 'absolute', top: 10, right: 10}} 
            >x</button>
          })}
        >3</button>

        <Modal
          isOpen={this.state.open}
          onAfterOpen={this.afterOpenModal}
          onRequestClose={this.handleClose.bind(this)}
          style={{backgroundColor: '#005'}}
          contentLabel="ようつべ"

          // close
          shouldCloseOnOverlayClick={this.state.shouldCloseOnOverlayClick}
          shouldCloseOnEsc={this.state.shouldCloseOnEsc}
        > 
          {this.state.closeButton}
          <iframe 
            width={560} 
            height={315} 
            src={`https://www.youtube.com/embed/${this.state.code}`}
            frameBorder="0" 
            allow="autoplay; encrypted-media" 
            allowFullScreen
          />
        </Modal>
    </div>
  }
}

react-dnd

バージョン
5.0.0
リポジトリ
https://github.com/react-dnd/react-dnd
デモ
コード
import React from 'react'
import { DragSource } from 'react-dnd'



export default class Index extends React.Component {
  constructor (props) {
    super(props)
    this.state = {files: []}
  }

  onDrop (files) {
    this.setState({files: [].concat(this.state.files).concat(files)})
  }

  render () {
    
    return <div className="component">
    
    </div>
  }
}

react-draggable

:バージョン
3.0.5
:リポジトリ
https://github.com/mzabriskie/react-draggable
:デモ

react-dnd と機能的にかぶってるんですが、個人的には使いやすかったので紹介しておきます。

コード
import React from 'react'
import Draggable from 'react-draggable'
import '@styles/react-draggable.styl'

const tellK = 'https://pbs.twimg.com/profile_images/775582814871752704/J1TaucBz_400x400.jpg'

export default class Index extends React.Component {
  constructor (props) {
    super(props)
    this.state = {movable: true}
  }
  handleStart (e, data) {
    console.log('移動開始', e, data)
  }
  handleDrag (e, data) {
    console.log('移動中', e, data)
  }
  handleStop (e, data) {
    console.log('移動終了', e, data)
  }
  render () {
    return <div className="component">
      <label>
        Movable
        <input 
          type="checkbox"
          checked={this.state.movable}
          onChange={() => this.setState({movable: !this.state.movable})}
        />
      </label>
      <Draggable
        handle=".drag-point"
        disabled={!this.state.movable}
        defaultPosition={{x: 0, y: 0}}
        onStart={this.handleStart}
        // ログが大量に出力されるためコメントアウト
        //onDrag={this.handleDrag}
        onStop={this.handleStop}
        // 移動範囲を相対座標で制限
        bounds={{top:0, bottom: 100, left: 0, right: 500}}
      ><div className="tellk">
        <img 
          className={this.state.movable ? 'movable': ''}
          src={tellK} width="100"
        />
        <div
          className="drag-point"
          style={{
            width: 20, height: 20,
            position: 'absolute', top: 42, left: 42,
            cursor: "move",
            display: this.state.movable ? 'block' : 'none',
            backgroundColor: '#000', opacity: 0.1,
          }}
        />
      </div></Draggable>
    </div>
  }
}

react-select

セレクトボックスを高機能にしたもの考えていただければ概ね間違っていないと思います。 jQuery だと select2 ってライブラリがありましたが、それの ReactJS 版です。

バージョン
2.0.0
リポジトリ
https://github.com/JedWatson/react-select
デモ
コード
import 'babel-polyfill'

import React from 'react'
import {default as Select, AsyncSelect} from 'react-select';

import '@styles/react-select.styl'

const answers = [
  {value: 1, label: 'crochaco'}, 
  {value: 2, label: 'chrohaco'},
  {value: 3, label: 'chrohako'},
  {value: 4, label: 'kurohako'},
  {value: 5, label: 'crohako'},
  {value: 6, label: 'chrohacho'},
  {value: 7, label: 'kurohaco'},
  {value: 8, label: 'crohaco'},
  {value: 9, label: 'chorohaco'},
]


export default class Index extends React.Component {
  constructor (props) {
    super(props)
    this.state = {answer: null}
  }

  render () {
    return <div className="component">
      <Select 
        className='selectbox'
        value={this.state.answer}
        onChange={answer => this.setState({answer})}
        options={answers}
        placeholder='正しいのはどれでしょう'
      />
      <div className="name-result">
      {this.state.answer ?
        this.state.answer.value === 8 ? 'あたり!' : 'はずれ'
      : ''}
      </div>
    </div>
  }
}


export class Async extends React.Component {
  constructor (props) {
    super(props)
    this.state = {selected: null}
  }

  async fetchResult () {
    const res = await fetch()
    const data = await res.json()
    return data
  }

  render () {

    return <div className="component">
      <AsyncSelect
        isClearable
        loadOptions={this.fetchResult.bind(this)}
        backspaceRemoves={true}
        onChange={selected => this.setState({selected})}
        onChange={selected => {this.setState({selected})}}
        value={this.state.selected}
        noOptionsMessage={({inputValue}) => inputValue ? 'No options' : 'Type to search'}
        placeholder="検索キーワード"
      />
    </div>
  }
}

備考

少し前に version 2.0.0 がリリースされました。

v2 の変更点:

value は value (id) だけを指定するのではなく label を含むオブジェクトで指定するようになった v && v.value みたいにしなくてよい

sort: data.sort || '' sort: data.sort ? data.sort.value : ''

zIndex が負ける場合は <Select> コンポーネント本体にクラスを指定して、それに対してスタイルを指定する クラスを指定しないと css-xxxxxxx (xxxxxxxは可変) のようなクラス名しかつかないので。

value は value (id) だけを指定するのではなく label を含むオブジェクトで指定するようになった v && v.value みたいにしなくてよい

sort: data.sort || '' sort: data.sort ? data.sort.value : ''

zIndex が負ける場合は <Select> コンポーネント本体にクラスを指定して、それに対してスタイルを指定する クラスを指定しないと css-xxxxxxx (xxxxxxxは可変) のようなクラス名しかつかないので。

AsyncSelect は import AsyncSelect from 'react-select/lib/Async' のようにインポートするようになった 1系では loadOptions は {options} キーでいろいろとラップする必要があったが、配列だけ返却すればいい

valueKey="id" labelKey="name"

filterOption はなくなった のですべて loadOptions で調整する 入力文字がないときにリクエストが発生しなくなった

async getGroups (i) {

const res = await dfetch(/api/groups/my/?search=${i}) const data = await res.json() return data.results .filter((option) => this.props.id != option.id) .map(d => {

System Message: ERROR/3 (<string>, line 232)

Unexpected indentation.
return {
value: d.id, label: <div>{d.name} <span className="option-id">{d.id}</span></div>

System Message: WARNING/2 (<string>, line 235)

Definition list ends without a blank line; unexpected unindent.

}

System Message: WARNING/2 (<string>, line 236)

Block quote ends without a blank line; unexpected unindent.

})

System Message: WARNING/2 (<string>, line 237)

Definition list ends without a blank line; unexpected unindent.

}

<AsyncSelect
loadOptions={this.getPrivateGroups.bind(this)} backspaceRemoves={true} onChange={parentPrivateGroup => this.setState({parentPrivateGroup})} value={this.state.parentPrivateGroup} noOptionsMessage={() => 'Type to search'} placeholder="Input parts of the private group name" />
import Select from 'react-select'
import 'react-select/dist/react-select.css';

<Select
  value={projects.sort}
  onChange={v => {this.props.set_projects({sort: v && v.value})}}
  options={sort_options}
  placeholder="Sort"
/>
import {Async as AsyncSelect} from 'react-select';
import 'react-select/dist/react-select.css';

<AsyncSelect ref="parent_group"
  loadOptions={this.getGroups.bind(this)}
  // 取得結果のうち表示したくないものがあるときは bool を返す関数を指定 (trueで見せる)
  filterOption={(option, string) => {return this.props.id != option.id}}
  // 取得結果のうち ID (option タグ の value属性)に あたるキーを指定
  valueKey="id"
  // 取得結果のうち 値 (option タグ の innerHTML) にあたるキーを指定
  labelKey="name"
  // バックスペースで選択したものが消せるか
  backspaceRemoves={true}
  onChange={v => {this.props.edit_parent_group(v)}}
  // 選択肢のデータを保存しておく場所
  value={data.parent_group}
  // 背景文字を表示するあれ
  placeholder="Choose something!!"
/>

react-dropzone

バージョン
5.1.0
リポジトリ
https://github.com/react-dropzone/react-dropzone
デモ
コード
import React from 'react'
import Dropzone from 'react-dropzone'

const myImage = 'https://pbs.twimg.com/profile_images/959293912690049025/xH9RPWDc_400x400.jpg'

export default class Index extends React.Component {
  constructor (props) {
    super(props)
    this.state = {files: []}
  }

  onDrop (files) {
    this.setState({files: [].concat(this.state.files).concat(files)})
  }

  render () {
    return <div className="component">
      <Dropzone onDrop={this.onDrop.bind(this)} size={150}>
        <ul className="preview">
          {
            this.state.files.map((file, index) => (
              <li key={index}>
                <img src={file.preview || myImage} />
                <div className="name">{file.name}</div>
              </li>
            ))
          }
        </ul>
        <p>Drag an image to upload</p>
      </Dropzone>
  	</div>
  }
}

dropzone という ドラッグアンドドロップでファイルをアップロードするためのライブラリです

import Dropzone from 'react-dropzone'

onDrop (type, files) {
  this.setState({photo: fileToObject(files[0])})
}

<Dropzone onDrop={this.onDrop.bind(this)} size={90}>
  <div className="dropArea">
    <div className="preview">
      <img src={this.state.photo && this.state.photo.src} className="profilePhoto" />
    </div>
    <p>Drag an image to upload</p>
  </div>
</Dropzone>

react-rnd

バージョン
8.0.2
リポジトリ
https://github.com/bokuweb/react-rnd
デモ
コード
import React from 'react'
import { Rnd } from 'react-rnd'

export default class Index extends React.Component {
  constructor (props) {
    super(props)
    this.state = {open: false}
  }

  handleClose () {
    this.setState({open: false})
  }

  render () {
    return <div className="component">
        <Rnd
          className="draggable-post" 
          dragHandleClassName='drag-area'
          size={{
            width: this.state.draggablePost.width || window.innerWidth * 0.7, 
            height: this.state.draggablePost.height || 200,
          }}
          onDragStop={(e, d) => {
            this.setState({draggablePost: {
              ... this.state.draggablePost, 
              x: d.x, 
              y: d.y,
            }})
          }}
          onResize={(e, direction, ref, delta, position) => {
            this.setState({draggablePost: {
              ... this.state.draggablePost, 
              width: ref.offsetWidth,
              height: ref.offsetHeight,
              ...position,
            }})
          }}
          resizeHandleWrapperClass='resize-handler-wrapper'
          resizeHandleStyles={{
            bottom: {bottom: -(this.state.draggablePost.scrollY || 0), left: 0},
            bottomLeft: {bottom: -(this.state.draggablePost.scrollY || 0), left: 0},
            bottomRight: {bottom: -(this.state.draggablePost.scrollY || 0), right: 0},
            left: {top: (this.state.draggablePost.scrollY || 0), left: 0},
            right: {top: (this.state.draggablePost.scrollY || 0), right: 0},
            top: {top: (this.state.draggablePost.scrollY || 0), left: 0},
            topLeft: {top: (this.state.draggablePost.scrollY || 0), left: 0},
            topRight: {top: (this.state.draggablePost.scrollY || 0), right: 0},
          }}
          style={{
            ... this.state.draggablePost.config.style.post,
          }}
        >
        
        </Rnd>
    </div>
  }
}

react-autocomplete

バージョン
1.8.1
リポジトリ
https://github.com/reactjs/react-autocomplete
デモ
コード
import React from 'react'
import AutoComplete from "react-autocomplete"

const members = [
  {name: 'crohaco', twitter: 'https://twitter.com/crohaco', icon: 'https://pbs.twimg.com/profile_images/959293912690049025/xH9RPWDc_400x400.jpg'},
  {name: 'shimizukawa', twitter: 'https://twitter.com/shimizukawa', icon: 'https://pbs.twimg.com/profile_images/1452813575/shimizukawa_half1_megane_180_400x400.png'},
  {name: 'takanory', twitter: 'https://twitter.com/takanory', icon: 'https://pbs.twimg.com/profile_images/192722095/kurokuri_400x400.jpg'},
  {name: 'tell-k', twitter: 'https://twitter.com/tell_k', icon: 'https://pbs.twimg.com/profile_images/775582814871752704/J1TaucBz_400x400.jpg'},
]

export default class Index extends React.Component {
  constructor (props) {
    super(props)
    this.state = {name: ''}
  }

  render () {
    return <div className="component">
      <AutoComplete 
        // 配列の中の要素から補完用の文字列を抽出する処理を書く
        getItemValue={(item) => item.name}
        items={members.filter(member => member.name.includes(this.state.name))}
        renderItem={(item, isHighlighted) =>
          <div style={{verticalAlign: 'middle', background: isHighlighted ? 'lightgray' : 'white' }}>
            <div style={{display: 'inline-block', minWidth: 200}}>{item.name}</div> 
            <a target="_blank" href={item.twitter}><img src={item.icon} style={{width: 30, height: 30}}/></a>
          </div>
        }
        wrapperStyle={{
          position: 'relative',
          border: 'solid 1px #800',
        }}
        menuStyle={{
          border: 'solid 2px #080',
          backgroundColor: '#dfd',
          zIndex: 2,
          position: 'absolute',
          top: 30,
          left: 0,
          overflow: 'auto',
          maxHeight: 100,
        }}
        value={this.state.name || ''}
        inputProps={{
          placeholder: "input name",
          style: {fontSize: 14, width: '100%', padding: 3},
        }}
        onChange={e => this.setState({name: e.target.value})}
        onSelect={name => this.setState({name})}
      />
    </div>
  }
}