Recompose: Setting state in lifecycle disables Recompose's "set state" functions

Created on 2 Jan 2018  ·  5Comments  ·  Source: acdlite/recompose

Consider the following test where we create a button and a message. Clicking on the button changes the message.

Initial State: The message renders to "HelloWorld"
Expected behaviour: Clicking the button changes the message to "HelloButton"
Actual behaviour: Clicking the button does not change the message.

Why use lifecycle componentDidMount? Lets say I want to async fetch data and populate the UI when the component mounts.

Here is the jest test:

import React from 'react';
import ReactDOM from 'react-dom';
import { compose, lifecycle, withHandlers, withState } from 'recompose';

import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });

const SimpleBtnMsgComp = compose(
  withState('message', 'setMessage', 'HelloStart'),
  lifecycle({
    componentDidMount() {
      this.setState({ message: 'HelloWorld' });
    },
  }),
  withHandlers({
    btnClick: ({ setMessage }) => event => {
      setMessage('HelloButton');
    },
  }),
)(({ message, btnClick }) => {
  return (
    <div className="theApp">
      <button id="btn1" onClick={btnClick}>
        Click Me!
      </button>
      <div id="msg">{message}</div>
    </div>
  );
});

test('SimpleBtnMsg changes the text after click', () => {
  const app = mount(<SimpleBtnMsgComp />);

  console.log(app.html());

  expect(app.find('#msg').text()).not.toEqual('HelloStart');
  expect(app.find('#msg').text()).toEqual('HelloWorld');

  app.find('button').simulate('click');

  console.log(app.html());

  expect(app.find('#msg').text()).toEqual('HelloButton');
});

Here is the output when run using jest:

 FAIL  src/__tests__/simpleBtnMsg.js
  ✕ SimpleBtnMsg changes the text after click (41ms)

  ● SimpleBtnMsg changes the text after click

    expect(received).toEqual(expected)

    Expected value to equal:
      "HelloButton"
    Received:
      "HelloWorld"

      42 |   console.log(app.html());
      43 |
    > 44 |   expect(app.find('#msg').text()).toEqual('HelloButton');
      45 | });
      46 |

      at Object.<anonymous> (src/__tests__/simpleBtnMsg.js:44:35)

  console.log src/__tests__/simpleBtnMsg.js:35
    <div class="theApp"><button id="btn1">Click Me!</button><div id="msg">HelloWorld</div></div>

  console.log src/__tests__/simpleBtnMsg.js:42
    <div class="theApp"><button id="btn1">Click Me!</button><div id="msg">HelloWorld</div></div>

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        4.1s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

Can anyone help explain this? Am I using lifecycle in the wrong way?

Most helpful comment

I've also written a more complex example using setTimout below. This is to emulate a more real world async fetch data scenario. Is there a better way of doing this?

import React from 'react';
import ReactDOM from 'react-dom';
import { compose, lifecycle, withHandlers, withState } from 'recompose';

import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });

const SimpleBtnMsg = (done, cb) =>
  compose(
    lifecycle({
      componentDidMount() {
        var that = this;
        setTimeout(() => {
          that.setState({ message: 'HelloWorld' });
          cb();
          done();
        }, 1000);
      },
    }),
    withState('message', 'setMessage', 'HelloStart'),
    withHandlers({
      btnClick: ({ setMessage }) => event => {
        setMessage('HelloButton');
      },
    }),
  )(({ message, btnClick }) => {
    return (
      <div className="theApp">
        <button id="btn1" onClick={btnClick}>
          Click Me!
        </button>
        <div id="msg">{message}</div>
      </div>
    );
  });

test('SimpleBtnMsg changes the text after click', done => {
  var app;
  const SimpleBtnMsgComp = SimpleBtnMsg(done, () => {
    expect(app.find('#msg').text()).toEqual('HelloWorld');

    app.find('button').simulate('click');

    console.log(app.html());

    expect(app.find('#msg').text()).toEqual('HelloButton');
  });
  app = mount(<SimpleBtnMsgComp />);

  console.log(app.html());

  expect(app.find('#msg').text()).toEqual('HelloStart');
});

Output in Jest is below:

  console.log src/__tests__/simpleBtnMsg.js:51
    <div class="theApp"><button id="btn1">Click Me!</button><div id="msg">HelloStart</div></div>

Error: Uncaught [Error: expect(received).toEqual(expected)

Expected value to equal:
  "HelloWorld"
Received:
  "HelloStart"]
    at reportException (/Users/simon/Space/Development/recompose-test/node_modules/jsdom/lib/jsdom/living/helpers/runtime-script-errors.js:66:24)
    at Timeout.callback [as _onTimeout] (/Users/simon/Space/Development/recompose-test/node_modules/jsdom/lib/jsdom/browser/Window.js:594:7)
    at ontimeout (timers.js:475:11)
    at tryOnTimeout (timers.js:310:5)
    at Timer.listOnTimeout (timers.js:270:5) { Error: expect(received).toEqual(expected)

Expected value to equal:
  "HelloWorld"
Received:
  "HelloStart"
    at /Users/simon/Space/Development/recompose-test/src/__tests__/simpleBtnMsg.js:41:37
    at /Users/simon/Space/Development/recompose-test/src/__tests__/simpleBtnMsg.js:16:11
    at Timeout.callback [as _onTimeout] (/Users/simon/Space/Development/recompose-test/node_modules/jsdom/lib/jsdom/browser/Window.js:592:19)
    at ontimeout (timers.js:475:11)
    at tryOnTimeout (timers.js:310:5)
    at Timer.listOnTimeout (timers.js:270:5)
  matcherResult:
   { actual: 'HelloStart',
     expected: 'HelloWorld',
     message: [Function],
     name: 'toEqual',
     pass: false } }
 FAIL  src/__tests__/simpleBtnMsg.js
  ✕ SimpleBtnMsg changes the text after click (1038ms)

  ● SimpleBtnMsg changes the text after click

    expect(received).toEqual(expected)

    Expected value to equal:
      "HelloWorld"
    Received:
      "HelloStart"

      39 |   var app;
      40 |   const SimpleBtnMsgComp = SimpleBtnMsg(done, () => {
    > 41 |     expect(app.find('#msg').text()).toEqual('HelloWorld');
      42 |
      43 |     app.find('button').simulate('click');
      44 |

      at src/__tests__/simpleBtnMsg.js:41:37
      at src/__tests__/simpleBtnMsg.js:16:11
      at Timeout.callback [as _onTimeout] (node_modules/jsdom/lib/jsdom/browser/Window.js:592:19)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        2.711s, estimated 3s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

All 5 comments

1) state property message at lifecycle hides state property message from withState after didMount. So it does not matter what value you will provide in setMessage.

2) Don't use lifecycle at all. Use React classes if you can't live without, it will be better and simpler.

@timkindberg wrote the following in the Recipes

Can you please elaborate how we can do the following without lifecycle and using React classes. PLEASE?

The other reason why I'm raising this as a bug is because the documentation states that this should work?

Any state changes made in a lifecycle method, by using setState, will be propagated to the wrapped component as props.

const { Component } = React;
const { compose, lifecycle, branch, renderComponent } = Recompose;

const withUserData = lifecycle({
  state: { loading: true },
  componentDidMount() {
    fetchData().then((data) =>
      this.setState({ loading: false, ...data }));
  }
});

const Spinner = () =>
  <div className="Spinner">
    <div className="loader">Loading...</div>
  </div>;

const isLoading = ({ loading }) => loading;

const withSpinnerWhileLoading = branch(
  isLoading,
  renderComponent(Spinner)
);

const enhance = compose(
  withUserData,
  withSpinnerWhileLoading
);

const User = enhance(({ name, status }) =>
  <div className="User">{ name }—{ status }</div>
);

const App = () =>
  <div>
    <User />
  </div>;

I've also written a more complex example using setTimout below. This is to emulate a more real world async fetch data scenario. Is there a better way of doing this?

import React from 'react';
import ReactDOM from 'react-dom';
import { compose, lifecycle, withHandlers, withState } from 'recompose';

import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });

const SimpleBtnMsg = (done, cb) =>
  compose(
    lifecycle({
      componentDidMount() {
        var that = this;
        setTimeout(() => {
          that.setState({ message: 'HelloWorld' });
          cb();
          done();
        }, 1000);
      },
    }),
    withState('message', 'setMessage', 'HelloStart'),
    withHandlers({
      btnClick: ({ setMessage }) => event => {
        setMessage('HelloButton');
      },
    }),
  )(({ message, btnClick }) => {
    return (
      <div className="theApp">
        <button id="btn1" onClick={btnClick}>
          Click Me!
        </button>
        <div id="msg">{message}</div>
      </div>
    );
  });

test('SimpleBtnMsg changes the text after click', done => {
  var app;
  const SimpleBtnMsgComp = SimpleBtnMsg(done, () => {
    expect(app.find('#msg').text()).toEqual('HelloWorld');

    app.find('button').simulate('click');

    console.log(app.html());

    expect(app.find('#msg').text()).toEqual('HelloButton');
  });
  app = mount(<SimpleBtnMsgComp />);

  console.log(app.html());

  expect(app.find('#msg').text()).toEqual('HelloStart');
});

Output in Jest is below:

  console.log src/__tests__/simpleBtnMsg.js:51
    <div class="theApp"><button id="btn1">Click Me!</button><div id="msg">HelloStart</div></div>

Error: Uncaught [Error: expect(received).toEqual(expected)

Expected value to equal:
  "HelloWorld"
Received:
  "HelloStart"]
    at reportException (/Users/simon/Space/Development/recompose-test/node_modules/jsdom/lib/jsdom/living/helpers/runtime-script-errors.js:66:24)
    at Timeout.callback [as _onTimeout] (/Users/simon/Space/Development/recompose-test/node_modules/jsdom/lib/jsdom/browser/Window.js:594:7)
    at ontimeout (timers.js:475:11)
    at tryOnTimeout (timers.js:310:5)
    at Timer.listOnTimeout (timers.js:270:5) { Error: expect(received).toEqual(expected)

Expected value to equal:
  "HelloWorld"
Received:
  "HelloStart"
    at /Users/simon/Space/Development/recompose-test/src/__tests__/simpleBtnMsg.js:41:37
    at /Users/simon/Space/Development/recompose-test/src/__tests__/simpleBtnMsg.js:16:11
    at Timeout.callback [as _onTimeout] (/Users/simon/Space/Development/recompose-test/node_modules/jsdom/lib/jsdom/browser/Window.js:592:19)
    at ontimeout (timers.js:475:11)
    at tryOnTimeout (timers.js:310:5)
    at Timer.listOnTimeout (timers.js:270:5)
  matcherResult:
   { actual: 'HelloStart',
     expected: 'HelloWorld',
     message: [Function],
     name: 'toEqual',
     pass: false } }
 FAIL  src/__tests__/simpleBtnMsg.js
  ✕ SimpleBtnMsg changes the text after click (1038ms)

  ● SimpleBtnMsg changes the text after click

    expect(received).toEqual(expected)

    Expected value to equal:
      "HelloWorld"
    Received:
      "HelloStart"

      39 |   var app;
      40 |   const SimpleBtnMsgComp = SimpleBtnMsg(done, () => {
    > 41 |     expect(app.find('#msg').text()).toEqual('HelloWorld');
      42 |
      43 |     app.find('button').simulate('click');
      44 |

      at src/__tests__/simpleBtnMsg.js:41:37
      at src/__tests__/simpleBtnMsg.js:16:11
      at Timeout.callback [as _onTimeout] (node_modules/jsdom/lib/jsdom/browser/Window.js:592:19)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        2.711s, estimated 3s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

Have you tried to use

this.props.setMessage('HelloButton');

in your lifecycle instead of

this.setState({ message: 'HelloWorld' });

?

lifecycle expands the chain of HOCs, so functions defined with withState HOC are available in the props.

There is no need to use react classes if you use recompose, just use recompose correctly. As my colleague @krazov suggested before, you could have used this.props.setMessage. You should not interact with state the react way because you are using recompose.

I've made a small jsfiddle you can play here: https://jsfiddle.net/r3ct0r/hzxkf5br/4/

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jethrolarson picture jethrolarson  ·  4Comments

astanciu picture astanciu  ·  3Comments

finom picture finom  ·  3Comments

nemocurcic picture nemocurcic  ·  3Comments

joncursi picture joncursi  ·  3Comments