質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.72%

React+Redux+Material-UI error : is not a function

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 248

khhhhh

score 1

はじめに

React + Redux + Material-UI で簡単なアプリケーションを開発しています。
現在はMaterial-UIのDrawerを実装しているのですが、
connect()でComponentに渡した関数を使用すると下記のようなエラーが発生します。
どなたかご教授お願い致します。

発生している問題・エラーメッセージ

TypeError: this.props.handleToggleDrawer is not a function
onClick
src/components/header/NavIcon.js:79
  76 | <IconButton
  77 |     color="inherit"
  78 |     aria-label="Menu"
> 79 |     onClick={() => this.props.handleToggleDrawer()}
  80 | ^   className={classes.navIcon}
  81 | >
  82 |     <MenuIcon />

該当のソースコード

/**
 * action/NavIconAction.js
 */
import { TOGGLE_DRAWER } from "../utils/actionTypes";

export const toggleDrawer = () => {
    return {
        type: TOGGLE_DRAWER,
    }
}
/**
 * reducers/NavIconReducer.js
 */
import * as actionTypes from '../utils/actionTypes'

const initialState = {
    open: false,
}

export default function NavIconReducer(state = initialState, action) {
    switch (action.type) {
        case actionTypes.TOGGLE_DRAWER:
            console.log('open: ' + !state.open)
            return Object.assign({}, state, {
                open: !state.open,
            })
        default:
            return state
    }
}
/**
 * reducers/index.js
 */
import { combineReducers } from 'redux';
import NavIconReducer from './NavIconReducer';
import ShareIconReducer from './ShareIconReducer';
import NotificationReducer from './NotificationReducer';

const reducers = combineReducers({
    NavIconReducer,
    ShareIconReducer,
    NotificationReducer
});

export default reducers;
/**
 * containers/NavIconContainer.js
 */
import { connect } from 'react-redux';
import { toggleDrawer } from '../actions/NavIconAction'
import NavIcon from '../components/header/NavIcon'

const mapStateToProps = state => ({
    open: state.NavIconReducer.open,
})

const mapDispatchToProps = dispatch => {
    return{
        handleToggleDrawer: () => dispatch(toggleDrawer())
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(NavIcon)
/**
 * components/header/NavIcon.js
 */
import React from 'react'
import { withStyles } from '@material-ui/core/styles'
import MenuIcon from '@material-ui/icons/Menu'
import Hidden from '@material-ui/core/Hidden'
import Drawer from '@material-ui/core/Drawer'
import Divider from '@material-ui/core/Divider'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemText from '@material-ui/core/ListItemText'
import InfoIcon from '@material-ui/icons/Info';
import HomeIcon from '@material-ui/icons/Home';
import AccountCircle from '@material-ui/icons/AccountCircle';
import LockOpen from '@material-ui/icons/LockOpen';
import HowToReg from '@material-ui/icons/HowToReg';
import { ListItemIcon } from '@material-ui/core';
import IconButton from '@material-ui/core/IconButton';

const drawerWidth = 250 // Width for Drawer

const styles = theme => ({
    navIcon: {
        [theme.breakpoints.up('md')]: {
            display: 'none',
        },
    },
    drawer: {
    },
    drawerPaper: {
        width: drawerWidth,
    },
});

class NavIcon extends React.Component {
    render() {
        const { theme, classes } = this.props

        const drawer = (
            <div>
                <Divider />
                <List>
                    <ListItem button>
                        <ListItemIcon><InfoIcon /></ListItemIcon>
                        <ListItemText primary="***** とは" />
                    </ListItem>
                </List>
                <Divider />
                <List>
                    <ListItem button>
                        <ListItemIcon><HomeIcon /></ListItemIcon>
                        <ListItemText primary="トップページ" />
                    </ListItem>
                </List>
                <List>
                    <ListItem button>
                        <ListItemIcon><AccountCircle /></ListItemIcon>
                        <ListItemText primary="マイページ" />
                    </ListItem>
                </List>
                <List>
                    <ListItem button>
                        <ListItemIcon><LockOpen /></ListItemIcon>
                        <ListItemText primary="ログイン" />
                    </ListItem>
                </List>
                <List>
                    <ListItem button>
                        <ListItemIcon><HowToReg /></ListItemIcon>
                        <ListItemText primary="新規会員登録" />
                    </ListItem>
                </List>
            </div>
        )

        return (
            <div>
                <IconButton
                    color="inherit"
                    aria-label="Menu"
                    onClick={() => this.props.handleToggleDrawer()}
                    className={classes.navIcon}
                >
                    <MenuIcon />
                </IconButton>
                <nav className={classes.drawer} aria-label="link">
                    <Hidden mdUp> {/* md未満 */}
                        <Drawer
                            variant="temporary"
                            anchor={theme.direction === 'rtl' ? 'right' : 'left'}
                            open={this.props.open}
                            onClose={() => this.props.handleToggleDrawer()}
                            classes={{
                                paper: classes.drawerPaper,
                            }}
                            ModalProps={{
                                keepMounted: true, // Better open performance on mobile.
                            }}
                        >
                            {drawer}
                        </Drawer>
                    </Hidden>
                    <Hidden smDown implementation="css"> {/* md以上 */}
                        <Drawer
                            variant="permanent"
                            open
                            classes={{
                                paper: classes.drawerPaper,
                            }}
                        >
                            {drawer}
                        </Drawer>
                    </Hidden>
                </nav>
            </div>
        )
    }
}

export default withStyles(styles, { withTheme: true })(NavIcon)
/**
 * components/header/index.js
 */
import React from 'react'
import { withStyles } from '@material-ui/core/styles'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/ToolBar'
import Typography from '@material-ui/core/Typography';
import NavIcon from './NavIcon'
import ShareIcon from './ShareIcon'
import logo from '../../logo.png'

const drawerWidth = 250 // Width for Drawer

const styles = theme => ({
    appBar: {
        width: '100%',
        marginLeft: drawerWidth,
        [theme.breakpoints.up('md')]: {
            width: `calc(100% - ${drawerWidth}px)`,
        },
    },
    toolBar: {
    },
    title: {
        margin: 'auto',
    },
    logo: {
        width: 20,
        paddingRight: 5,
    },
})

const Header = ({ classes }) => (
    <div className={classes.header}>
        <AppBar className={classes.appBar}>
            <Toolbar className={classes.toolBar}>
                <NavIcon />
                <Typography className={classes.title}>
                    <img src={logo} alt="Header logo" className={classes.logo} />
                    ぐるめなう
                </Typography>
            </Toolbar>
        </AppBar>
    </div>
)

export default withStyles(styles, { withTheme: true })(Header)
/**
 * App.js
 */
import React from 'react';
import { Route, Switch } from 'react-router-dom'
import { makeStyles } from '@material-ui/core/styles'
import CssBaseline from '@material-ui/core/CssBaseline'
import Header from './components/header'
import About from './components/about'
import Top from './components/top'
import MyPage from './components/mypage'
import Login from './components/login'
import Signup from './components/signup'

const useStyles = makeStyles(() => ({
    root: {
    },
}))

function App() {
    const classes = useStyles();

    return (
        <CssBaseline>
            <div className={classes.root}>
                <Header />
                <Switch>
                    <Route exact path="/" component={Top} />
                    <Route exact path="/mypage" component={MyPage} />
                    <Route exact path="/login" component={Login} />
                    <Route exact path="/signup" component={Signup} />
                    <Route exact path="/about" component={About} />
                </Switch>
            </div>
        </CssBaseline>
    );
}

export default App;
/**
 * index.js
 */
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux';
import App from './App'
import { createStore } from 'redux';
import reducers from './reducers';

const store = createStore(reducers)

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
)

試したこと

MenuIconをクリックすると上記のエラーが発生します。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • jun68ykt

    2019/09/15 22:46

    以下、質問への追記要望が一点 (1) と、ソースコードを拝読して疑問に思ったこと二点、(2)(3) です。

    (1) App.js の冒頭で

    import NavIcon from './NavIcon'

    とあります。これの意味するところは、ソースコードの入っているディレクトリーの中で、 App.js (や index.js ) と同じ階層に、 NavIcon.js というソースファイルがあるということでしょうか?もしあるのであれば、そのソースも質問に追記頂きたいです。

    (2) containers/NavIconContainer.js で export default している、 Redux にconnectされたNavIconが、どこから import されているのかが、現状のソースコードを読む限りでは不明です。

    (3) containers/NavIconContainer.js の mapStateToProps で、state.MenuIconReducer.open が props の open として渡ってくるような意図で書かれているが、 reducers/index.js でcombineされるのは、以下

    {
    NavIconReducer,
    ShareIconReducer,
    NotificationReducer
    }

    となっていて、MenuIconReducer は無いので、 state.MenuIconReducer は undefined になり、 state.MenuIconReducer.open で
    Cannot read property 'open' of undefined
    のエラーになるのでは?と思いました。

    キャンセル

  • khhhhh

    2019/09/15 23:29

    ご回答有り難うございます!
    申し訳ございませんが、すべて誤植です…。変更させて頂きました。

    (1)App.jsの中身は誤植です。編集いたしました。
    (2)NavIconはcomponents/header/NavIcon.jsからimportしています。
    componentsは共通のheader部分と、ページ単位で分けています。
    ちなみにheaderフォルダの中身はindex.jsとNavIconなどが存在しており、その全てをheader/index.jsでまとめています。
    (3)それも誤植です。state.NavIconReducer.openですね…。編集いたしました。

    以上です。煩わしくて申し訳ございません。よろしくお願い致します。

    キャンセル

  • khhhhh

    2019/09/15 23:35 編集

    App.js
    index.js
    components
    -header
    --index.js
    --NavIcon.js
    reducers
    -index.js
    -NavIconReducer.js
    containers
    -NavIconContainer.js
    actions
    -NavIconAction.js
    util
    -actionTypes.js

    キャンセル

  • jun68ykt

    2019/09/15 23:50

    追記および修正ありがとうございます。回答しました。

    キャンセル

回答 1

checkベストアンサー

+2

こんにちは

components/header/index.js の冒頭で

修正前:

import NavIcon from './NavIcon'


としていますが、これによって import されるのは、 components/header/NavIcon.js に記載されている NavIcon コンポーネントですが、これは Redux に connect される前のコンポーネントです。
つまり、 containers/NavIconContainer.js にて redux に connect した後の NavIcon が使われていないです。これだと、props に handleToggleDrawer は渡されてこないので、 this.props.handleToggleDrawer()
とすると、ご質問にあるとおり、

this.props.handleToggleDrawer is not a function

のエラーになります。そこで、冒頭挙げた、修正前のimport行を以下のように修正してみるといかがでしょうか?

修正後:

import NavIcon from '../../containers/NavIconContainer'

追記

ソースコードのディレクトリ構造に、 components/ と  containers/ というサブディレクトリーを作ったときに、どのようにコンポーネントを類別して、 components/ と  containers/ に振り分けていくか?というのはけっこう悩みどころかと思います。

小生の過去の react + redux の業務経験ですと、あるプロジェクトでは、 

  • components/ にはredux にconnect しないコンポーネントを書き、components/ からimport したコンポーネントを redux にconnect させたコンポーネントを containers/ に入れる。(したがって components/ の中に、 redux にconnectしたコンポーネントを置くのは禁止)

というルールだったところもありますし、

  • components/ には部品的なものを入れて containers/ には、部品を配置した(いわば、"大きな" )コンポーネントを入れておく

というルールだったところもあります。 

containers/ には、その名のとおり「コンテナ(=容器)であるコンポーネント」を入れておくわけですが、この、"コンテナであるコンポーネント" とそうでないものとの2つに大別するという考え方は、Redux 作者のひとりであるDan Abramovさんの以下の記事が原典かと思います。

個人的には、上記に箇条書きで書かれている、 presentational components と container components それぞれの特徴を、満たす条件ととらえてすべてを厳密に守ろうとすると、融通がきかなくなってしまいがち、という所感を持っています。(実際、上記の記事は、はじめ2015年に書かれたものですが、その後、冒頭にUpdate from 2019 として追記があり、追記の最後に 

This text is left intact for historical reasons but don’t take it too seriously.

とDan氏自身が書いています。)

とはいえ、今でもひとつの目安にはなるので、上記の記事を、ご質問に挙げられているコードに適用してみると、container components の特徴の中には、

  • May contain both presentational and container components inside but usually don’t have any DOM markup of their own except for some wrapping divs, and never have any styles.

というものがありますが、これに沿うと、このご質問にある <Header /> も <NavIcon /> を含んでいるので、containerコンポーネントである、と言えなくもないです。

ご質問のコンポーネント構成では、

  • components/ にあるコンポーネント Xxx を Redux にconnectさせるソースファイルを XxxContainer.js として containers/ に作成する。

というルールにされているのかなと思いました。これはこれでアリな方針でありますが、これ以外の代替案として、

  • まず App から import されている以下

Header, About, Top, MyPage, Login, Signup

は、 container とする。これの根拠となるのは、「App から直接 import されるものは、ざっくり言って、大きな部品と考えて差し支えないから」という考え方です。

  • 上記 6 個のcontainerコンポーネントから import されるコンポーネントで、少なくない数の部品的なコンポーネントをimport して、ひとつのまとまった外観と機能を提供するものも containerコンポーネントとする。

  • 上記以外は、 ひとまず、components/ に入れておき、 その後の修正で、presentational component から container component に "格上げ" してもいいかなと思えたものは、その時点で適宜 containers/ に移動

  • components/ に入れておくものは Redux に connect してはダメというルールを、厳格に守ったほうがいいかはどうかはケースバイケースと考える。

特に、上記のリストの最後の考え方を採用してしまうと、

  • container なのか (presentationalな、)component なのかの判別に、redux に connectさせるかどうかは関係ない(少なくとも、判別の条件として、最優先されるわけではない。)

ということになるわけですが、これを「そんなことを認めたら、収拾がつかなくなるので認められない」と考えるか、「redux に connectさせるかどうかに縛られるよりも、 containerは大きな容れ物で、 component は containerに詰められる部品と考えたほうが、開発作業上のメリットが大きい」という考えを採用するかは、プロジェクトによって違っていました。

最後の考え方を採用すると、 components/header/NavIcon.js を redux に connect させたものを export してよいことになるので、以下のようにも書いてよいことになります。

/**
 * components/header/NavIcon.js
 */
import React from 'react'

・・・

class NavIcon extends React.Component {
・・・
}

// withStylesを適用したNavIconをいったん変数に入れておく。
const StyledNavIcon = withStyles(styles, { withTheme: true })(NavIcon)

// 以下にて、 StyledNavIcon を redux に connectして、これを export default する。
const mapStateToProps = state => ({
    open: state.NavIconReducer.open,
})

const mapDispatchToProps = dispatch => {
    return{
        handleToggleDrawer: () => dispatch(toggleDrawer())
    }
}

// 上記の StyledNavIcon を redux にconnectする。
export default connect(mapStateToProps, mapDispatchToProps)(StyledNavIcon)

そして、 containers/ のほうに移動した、 header/index.js にて、

import NavIcon from '../../components/header/NavIcon'


として、redux に connect済みのNavIconを components/ からimportして使います。

もちろん上記の代替案のほうがいいといっているわけではありません。最終的にはメンバーの考えをもとにフロントエンドのリーダーが決定すべき、チームの方針かなと思います。

以上、参考になれば幸いです。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/09/16 00:10

    なるほど!
    これは自分だけでは一生解決できていませんでした笑
    Redux難しい…。
    本当に助かりました!有り難うございました!

    キャンセル

  • 2019/09/16 01:09

    どういたしまして。解決されたようでよかったです👏
    参考までに、containers/ と components/  とに、どのようにコンポーネントを類別していくか、について追記しました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.72%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる