Instantly share code, notes, and snippets.

Embed
What would you like to do?
recomposeの使い方を学んでHOCと仲良くなる

recomposeの使い方を学んでHOCと仲良くなる

この記事はコンポーネント内のロジックを外だしたり、コンポーネントを再利用できるrecomposeの使い方をドキュメントを参考にリアルワールドで使えることを目的とした 初心者向けの記事です。

blog

recompose

recomposeのcomponentFromPropsの使い方

http://kenjimorita.jp/wp-content/uploads/2018/05/27322DB3-0300-4DA2-BB79-183F853045DC.jpeg

※こちらはrecomposeをより深く理解したい自分のための勉強mdです

※今後も更新予定(2018/6/16更新)

※もしお役に立てたらスターをポチお願い


recomposeは見た目的にとっつきにくかった。

ドキュメントは端的に書いてあった。端的すぎた。

日本語のブログはあるし、分かるのだけれど、なんかリアルではない気がした。

理解してきたので説明できるレベルになるべく臆面もなくここに書く。

「裸のコンポーネントが服を着る」 で例える

裸のコンポーネント・・・BaseComponent

服・・・recomposeが提供するAPI

let BaseComponent = ({someProp}) => <div>fafa</div> //裸のコンポーネント
BaseComponent = recomposeFun(//服を着終わったcomponentが返ってくるのでletとして再代入を許している
    ...some//服1を着る 
    ...some//服2を着る
)(BaseComponent) //服を着させる「裸のComponent」を渡している

export BaseComponent//服を着たComponentを外部へ

上のような目線を持ったことで理解が早くなった

裸のComponent(BaseCompnent)が服を着て(ラッップされて)別の実装やpropsを持ったComponentを返す

何が嬉しいか

裸のコンポーネントはいろいろなところで再利用。

着る服を変えることで「似ているけどちょっと違うコンポーネント」を作ることができる(実装やpropsを変えられる)

既存コードを変更することなく、再利用しやすい

また

ロジック(もしprops.isNullが trueだったら、など)を抽出、それも別のところで再利用できる

例えば

//component内
render (){
  return (<div>りんごがある</div>)
}
//
//親から渡されるpropsでviewを変えたい時、ロジックが入り込む
//例えば
render (){
  return (<div>{this.props.isNull ? <div>りんごがない</div> : <div>りんごがある</div>}</div>)
}

別のところで再利用が難しくなる。 上のcomponent内でしていることを簡単に切り出す

let BaseComponent = ({isNull}) => something... //isNullは渡ってきている。呼び出し側は <BaseComponent isNull={${何か親からもらった変数}}>
const isNull = ({isNull}) => isNull//再利用可能
const  NullDiv = () => <div>りんごがない</div>//外出ししたことで再利用可能
const Div = () =>  <div>りんごがある</div>//外出ししたことで再利用可能
const EvalutedBaseComponent = branch(//API①
 isNull,//切り出された実装
 NullDiv,
 Div
)(BaseComponent)//裸 BaseComponentで持っているpropsは①APIの中で受け取れる

export {evalutedBaseComponent, BaseComponent} //どちらも使える。

最初何しているかわからなかったがこのようにみることができた

本題...

componentFromProps

いろいろ使っているけどこれまだ使ったことないrecomposeのAPI(componentFromProps)をどういう時使いたくなるか考えながら示してみる

下記はrecomposeを使わない例からcomponentFromPropsを使うところまで記述

テキスト違いの同じようなボタンを作ろうとした時、下記のように作るのは一番きつい。。DRY

//Button.js
export const ToFormButton = (): React.Node  => (
    <div className='toFormBtn'>
        <a href='#contact' className='arrow'>こちら</a>
    </div>
)
//textだけ違う
export const ToFormButtonText = (): React.Node  => (
    <div className='toFormBtn'>
        <a href='#contact' className='arrow'>こちらから</a>//diff
    </div>
)
///app.js
import {ToFormButton, ToFormButtonText} from '../component/Button';

<ToFormButtonText />
<ToFormButton />

引数で一つで管理するようにする

//Button.js
export const ToFormButton = ({text}): React.Node  => (
    <div className='toFormBtn'>
        <a href='#contact' className='arrow'>{text}</a>
    </div>
)

///app.js
<ToFormButton text="こちらからです" />
<ToFormButton text="こちら" />

上記だとlinkも変わった時変更する必要がある。

//Button.js
export const ToFormButton = ({text, link}): React.Node  => (
    <div className='toFormBtn'>
        <a href={link} className='arrow'>{text}</a>
    </div>
)


/////引数が増える。。 
//app.js
<ToFormButton link="http://kenjimorita.jp" text="こちらからです" />
<ToFormButton link="http://kenjimorita.jp/javascript"text="こちら" />

propsが渡ってこないとエラーなのでdefault指定する

//Button.js
export const ToFormButton = ({text = "こちら", link = "http://kenjimorita.jp"}): React.Node  => (
    <div className='toFormBtn'>
        <a href={link} className='arrow'>{text}</a>
    </div>
)
//app.js
<ToFormButton text="こちらからです" />
<ToFormButton link="http://kenjimorita.jp/javascript" />
///styleも変えたくなったら?どんどんきつくなる

recompose化

//Button.js
import {compose, defaultProps} from 'recompose'

let ToFormButton = ({classstyle, link, text}): React.Node  => (
    <div className={classstyle}>
        <a href={link} className='arrow'>{text}</a>
    </div>
)

ToFormButton = compose(
    defaultProps({
        classstyle: 'toFormBtn',
        text:"こちら",
        link:"http://kenjimorita.jp"
    })
)(ToFormButton)
export {ToFormButton};

//app.js
//変更なし
<ToFormButton text="こちらからです" />
<ToFormButton link="http://kenjimorita.jp/javascript" />

aタグを共通化したい(外だししたい)場合は?

//Button.js
let ToFormButton = ({classstyle, link, text}): React.Node => (
    <div className={classstyle}>
        <a href={link} className='arrow'>{text}</a>
    </div>
)

let ALink = ({link, text}) => <a href={link} className='arrow'>{text}</a>

ToFormButton = compose(
    defaultProps({
        classstyle: 'toFormBtn',
        text:"こちら",
        link:"http://kenjimorita.jp"
    })
)(ToFormButton)

export {ToFormButton, ALink};//diff

さらに変更

//Button.js
let ToFormButton = ({classstyle, link, text}): React.Node => (
    <div className={classstyle}>
        <ALink link={link} className='arrow' text={text} />
    </div>
)

let ALink = ({link, text}) => <a href={link} className='arrow'>{text}</a>

他には、children。今更結構きつい修正だ

//Button.js
let ToFormButton = ({children, classstyle, link, text}): React.Node => (
    <div className={classstyle}>
        {children}
    </div>
)
let ALink = ({link, text}) => <a href={link} className='arrow'>{text}</a>


//app.js
<ToFormButton>
    <ALink link={"http://kenjimorita.jp"} text={"こちらです"} />
</ToFormButton>
<ToFormButton>
    <ALink link={"http://kenjimorita.jp/javascript"} text={"こちらから"} />
</ToFormButton>

こちらもcomposeして委譲するが。。。

//Button.js
let ToFormButton = ({children, classstyle}): React.Node => (//diff
    <div className={classstyle}>
        {children}
    </div>
)

let ALink = ({link, text}) => <a href={link} className='arrow'>{text}</a>

ALink = compose(//diff
    defaultProps({
        text:"こちら",
        link:"http://kenjimorita.jp"
    })
)(ALink)

ToFormButton = compose(//diff
    defaultProps({
        classstyle: 'toFormBtn',
    })
)(ToFormButton)


//app.js
<ToFormButton>
    <ALink text={"こちらです"} />
</ToFormButton>
<ToFormButton>
    <ALink link={"http://kenjimorita.jp/javascript"} />
</ToFormButton>

抽象化して共通のdefaultPropsを持たせる

//Button.js
let enhancer = defaultProps({//共通で使う
    text:"こちら",
    link:"http://kenjimorita.jp",
    classstyle: 'toFormBtn',
})
let Button = enhancer(componentFromProp("component"));//componentFromPropを使って、呼び出し元のcomponent属性にrenderするcomponentを指定する

let ToFormButton = ({link, text, classstyle}): React.Node => (
    <div className={classstyle}>
       <a href={link} className='arrow'>{text}</a>
    </div>
)
let ALink = ({link, text}) => <a href={link} className='arrow'>{text}</a>

export {ToFormButton, ALink, Button};


///
import {ToFormButton, Button, ALink } from '../component/Button';
//Buttonとして抽象化
//class ALink extends Button{}のようなイメージ。

<Button component={ALink} />//component指定
<Button component={ToFormButton} />//component指定


defaultPropsを変えたい??

enhancerが他でも使われている時

let enhancer = defaultProps({
    text:"こちら",
    link:"http://kenjimorita.jp",
    classstyle: 'toFormBtn',
})

let enhancerDefault = defaultProps({
    text:"こちらからお願いします",
})

let Button = enhancerDefault(enhancer(componentFromProp("component")));

//嫌い

番外編

何かpropsが持つboolean値でrenderを変える

1.評価する関数を作る 2.branchを呼ぶ 3.compose内にセット(composeしたいものがなければ返り値をそのままexport)

import {renderComponent, nest, branch, compose, defaultProps,renderNothing,  componentFromProp} from 'recompose'

let isAlink = ({isAlink}) => isAlink;//propsが渡ってきます

let brancedByisAlink = branch(
    isAlink,//評価する関数
    renderComponent(ALink),//trueの際のcomponentをrenderComponentで呼ぶ
    //falseの際はここだが省略もできる//もしくはrenderNothing
)

let enhancer = compose(
    defaultProps({
    text:"こちら",
    link:"http://kenjimorita.jp",
    classstyle: 'toFormBtn',
    }),
    brancedByisAlink,//判定してcomponentを返す関数
)

//app.js
<Button component={ALink} isAlink={true} />
<Button component={ToFormButton} isAlink={false} />

nestを使ってwrapするDOMをpropsの値によって変える

let isAlink = ({isAlink}) => isAlink;
let DivWrap = ({children, classstyle}) =>(
    <div className={classstyle}>
        {children}
    </div>
)
let liWrap = ({children, classstyle}) =>(
    <li className={classstyle}>
        {children}
    </li>
)

let ToFormButton = ({link, text, classstyle}): React.Node => (
    <div className={classstyle}>
       <a href={link} className='arrow'>{text}</a>
    </div>
)
let ALink = ({link, text}) => <a href={link} className='arrow'>{text}</a>

let brancedByisAlink = branch(
    isAlink, //isAlinkがtrueの時 li > a
   renderComponent(nest(liWrap, ALink)),//nestの引数に親となるcomponentを左から指定 li > a 
   renderComponent(nest(DivWrap, ToFormButton)),//親を左から指定 div > a
)

let enhancer = compose(
    defaultProps({
    text:"こちら",
    link:"http://kenjimorita.jp",
    classstyle: 'toFormBtn',
    }),
    brancedByisAlink,
)

let Button = enhancer(componentFromProp("component"));



export {ToFormButton, ALink, Button};

debugger ?

import {withProps, compose, defaultProps, ....} from 'recompose'

....something...

let enhancer = compose(
    defaultProps({
    text:"こちら",
    link:"http://kenjimorita.jp",
    classstyle: 'toFormBtn',
    }),
    withProps(console.log),//console.log(props)
    brancedByisAlink,
)

flattenProp

propsの値がネストする時フラットにする 例えば

let enhancer = compose(
    withProps({
        user: {
            id: 1,
            name: "kenji",
            address: "tokyo"
        }
    })
)
let Author = ({user: {id, name, address}}) => (//渡す時深くなる
    <React.Fragment>
        {name} {id} , {address}
    </React.Fragment>
)
Author = enhancer(Author)
export {Author};

//app.js
<Author />

これが

let enhancer = compose(
    withProps({
        user: {
            id: 1,
            name: "kenji",
            address: "tokyo"
        }
    }),
    flattenProp("user")
)

let Author = ({id, name, address}) => (
//ここのがprops.id、name、addressを持つようになる。props.user.id, props.user.name等としても持っている ex: Props: {id, name, address, user: {id, name, address}}
   <React.Fragment>
        {name} {id} , {address}
    </React.Fragment>
)

renameProp

let enhancer = compose(
    withProps({
        user: {
            id: 1,
            name: "kenji",
            address: "tokyo"
        }
    }),
    renameProp("user", "admin")//userをadminに変更 use caseはどんな時か、、、
)

let Author = ({admin: {id, name, address}}) => (//addmin
    <React.Fragment>
        {name} {id} , {address}
    </React.Fragment>
)

Author = enhancer(Author)

withPropsOnChange

withPropsOnChange() is just a shortcut for compose(shouldUpdate(...), mapProps(...))

withPropsOnChange(Array<string> | func: (props:Object, nextProps:Object) => boolean, createFunc(ownProps => Object))

第一引数にArrayを渡すと要素にあるpropsが変更されたら第二引数で渡されたcreateFuncが実行される
or
第一引数にpropsとnextPropsを引数にもつ関数を渡し、trueが返されたらcreateFuncが実行される

特定のpropsが更新された場合だけ実行することでコストがかかる計算を毎度しない。

WIP

real world

1
let Content = props => <Child {...props} />;
const isLoading = ({ isFetching }) => isFetching;
const nullTest = ({ result }) => result === null

const nodeta = branch(nullTest, renderComponent(Nodata));

const loading = branch(isLoading, renderComponent(Loading));

Content = compose(
  setDisplayName('Report'),
  lifecycle({
    componentDidMount() {
      this.props.request();
    }
  }),
  loading,
  nodeta,
  component => component
)(Content);

2

migrate class like to HOC

/////////////////////////////////////////////////
//from Class like
////////////////////////////////////////////////
class Mailaddress extends Component {
  componentDidMount() {
    this.props.dispatch(something);
  }
  render() {
    return <MailaddressForm {...this.props} />;
  }
}

Mailaddress.defaultProps = {
  text: 'some'
};

const mapDispatchToProps = dispatch => ({
  onSubmit(values) {
    dispatch(something)
    );
  },
  dispatch
});

const mapStateToProps = state => ({
  data: selector.email(state)
});

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Mailaddress));


/////////////////////////////////////////////////////////
to HOC
/////////////////////////////////////////////////////////
import {defaultProps, setDisplayName, compose, lifecycle} from 'recompose'
const mapDispatchToProps = dispatch => ({
  onSubmit(values) {
    dispatch(something);
  },
  dispatch
});
const mapStateToProps = state => ({
  data: selector.email(state)
});
let Mailaddress = compose(
  defaultProps({
    text: 'some'
  }),
  setDisplayName("Mailaddress"),
  connect(mapStateToProps, mapDispatchToProps),
  lifecycle({
    componentDidMount() {
      this.props.dispatch(something);
    }
  }),
  withRouter
)(MailaddressForm)
export default Mailaddress

3

onClickなどをwithHandlersで扱う

withHandlersはイベントをmapingしたオブジェクトを引数に取る。 それぞれの値はpropsを受け取り、イベンドを受け取る関数を返す

withHandlers({
 onClick: (props) => (event) => {
      event.preventDefault()
      props.someAction(event.target.value)
 }
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment