Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setState doesn't trigger rerender #134

Merged
merged 11 commits into from
Jun 22, 2016
Merged

Conversation

saschatimme
Copy link
Contributor

This pull request should solve the issues in #51.

The previus method was basically, that we keep the state in memory and at every call of setState we render everything again. The problem is that this breaks animations as well as other "dom-dependent"actions.

This would have the following breaking API changes:

Instead of:

'count' in state || setState({ count : 1})

it would just be:

setState({ count : 1})

Another breaking change would be, that instead of:

const mockData = require('./mocks');
<Message content={mockData.hello}/>

you would have to call require directly in the component:

<Message content={require('./mocks').hello}/>

The reason is that I split the example code at the (single) react component. Everything before will be executed in the componentWillMount and the rest in the rendermethod of a react component (calling setState in a render method is not possible).

Would love to hear some feedback.

P.S.: The examples seem to be broken because of the "dog-names" packages, which runs into an error.

// we split the setup of the state and the react component, since calling setState
// in render is not allowed.

const splitIndex = compiledCode.indexOf('React.createElement');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it work with ES6 classes and functional components?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my examples does it work. The React.createElement () is just the result after the JSX transformation. So <Button /> becomes React.createElement(button,...)

@sapegin
Copy link
Member

sapegin commented Apr 26, 2016

Thank you very much, this is very cool!

'count' in state || setState({ count : 1})

It was a hack so I won’t miss it ;-)

<Message content={require('./mocks').hello}/>

It’s fine when you need it only once (I think in most of the cases). Any way to reuse the required module?

@mik01aj
Copy link
Collaborator

mik01aj commented Apr 26, 2016

I don't like this implicit separation of code into componentWillMount and render. And, to be honest, I don't have an idea how to make it explicit in a clean way. I just think that this feature will be confusing. Right now the semantic of a code example is simple: it executes like a regular function, and the last expression is the result, like in eval. When I define a variable and can't access it one line below, then something is broken.

@sapegin
Copy link
Member

sapegin commented Apr 26, 2016

@mik01aj I agree but it fixes more things than breaks.

@mik01aj
Copy link
Collaborator

mik01aj commented Apr 26, 2016

For me it will break many examples, I use mocks pretty often. For example:

var mocks = require('./mocks');
<div>
    <Avatar user={ mocks.exampleUser } />
    <Avatar user={ mocks.exampleUserWithoutAvatar } />
    <Avatar user={ mocks.exampleDeletedUser } />
    <Avatar user={ mocks.exampleUserWithInvalidAvatarUrl } />
</div>

@sapegin
Copy link
Member

sapegin commented Apr 26, 2016

Maybe we could “hoist” setState calls — move only them to the componentWillMount? It’s more complicated and fragile but might work for more common use cases.

@mik01aj
Copy link
Collaborator

mik01aj commented Apr 26, 2016

Let's think again.

We're talking about example usages of a component. If, in order to use a component, you need some special initialization in componentWillMount, then your component is not really reusable imho. getInitialState should be enough.

So maybe all we need is just some way to set the initial state, and then everything else could be done in render?

So I would propose this:

intialState = {x:1}
<button onClick={ () => setState({x: state.x + 1}) }>{ state.x }</button>

We could then run this once to get the initial state, and then just put this in render.

@sapegin
Copy link
Member

sapegin commented Apr 26, 2016

Actually I like this idea very much ;-)

@saschatimme
Copy link
Contributor Author

We should probably also consider the case that somebody requires a module and uses this to initiate the state and his component as well.

For this we need to make some assumptions / heuristics about the example codes.

I will give it a try in the next days :)

@mik01aj
Copy link
Collaborator

mik01aj commented Apr 26, 2016

@saschatimme I think it will also work this way. That's what I think:

var DemoComponent = React.createClass({
    getInitialState: function () {
         var initialState = {};
         eval(EXAMPLE_CODE);
         return initialState;
    },
    render: function () {
         return eval(EXAMPLE_CODE);
    },
});

@saschatimme
Copy link
Contributor Author

I got it running with the idea of @mik01aj, thanks!

I also added a error if the old syntax is used, since the setState would now cause a stack overflow.

Maybe you could could give this version a try with your examples and let me know if I overlooked something :)

@sapegin
Copy link
Member

sapegin commented May 25, 2016

I need to try it one day.

@tizmagik
Copy link
Collaborator

tizmagik commented Jun 8, 2016

Any update on this?

@sapegin
Copy link
Member

sapegin commented Jun 8, 2016

@tizmagik Sorry didn’t have time to check it.

@mik01aj
Copy link
Collaborator

mik01aj commented Jun 9, 2016

I tested it. It seems to fix the animations, but on the other hand, it breaks examples like this:

var ButtonToy = React.createClass({
    render: function () {
        return <Button>Click me!</Button>;
    },
});
<ButtonToy /> 

They fail with this error:

SyntaxError: Unexpected token )

I'm OK with this (I mean, I can update my examples so they don't create components), but this is a breaking change.

See also comments in the code. My biggest concern is this code splitting, it is a bad, bad idea. It will break examples like this:

let a = <div className="dot" />;
<Container>{ a }{ a }{ a }<div>{ a }{ a }</div></Container>

if (this.state.error) {
return <pre className={s.playgroundError}>{this.state.error}</pre>;
}
const setState = partialState => this.setState(partialState);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setState in React is defined as void setState(function|object nextState, [function callback]); you missed the 2nd argument.

@sapegin
Copy link
Member

sapegin commented Jun 9, 2016

Then we can merge it and combine with hiding code examples by default in a single major release.

// the code contains the setup of the state and the react component to render.
// we split the setup of the state and the react component;

const splitIndex = compiledCode.indexOf('React.createElement');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bad idea. And after the discussion in the PR, I thought we don't need any splitting anymore.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I also thought that we’ve found a better solution.

@sapegin sapegin mentioned this pull request Jun 9, 2016
const renderCode = `
var initialState = {};
${compiledCode.substring(0, splitIndex)}
return ${compiledCode.substring(splitIndex)};
`;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to pass the setState Function and the state into our component, we have to use new Function and cannot work with eval (this happens ins this.props.evalInContext). The current implementation uses the fact, that eval returns the result of the last statement, but for a with new Function generated function this is not the case. Hence to get a proper render Function with a return value, we have to insert somehow a return statement for the last component. This is the only reason splitIndex exists.

Copy link
Collaborator

@mik01aj mik01aj Jun 9, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just add more arguments to evalInContext instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean? I don't see what extra argument could solve the problem.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In examples.loader, there was this line:

'function(code, setState) {' +

The setState callback was passed to the component and it worked fine. If you needed to pass something more, you could just add arguments to this function. Why did you change it to use new Function()? Without that change, things would get simpler here.

@sapegin
Copy link
Member

sapegin commented Jun 15, 2016

Would be very nice merge this PR. @saschatimme are you going to finish it?

@saschatimme
Copy link
Contributor Author

Sorry I was quite busy the last two weeks but I will try to find a solution that doesn't involve any code splitting this weekend.

@sapegin
Copy link
Member

sapegin commented Jun 15, 2016

@saschatimme Cooool!

@saschatimme
Copy link
Contributor Author

@sapegin I removed the code splitting and the counter examples from mik01aj are now working

code = `
const state = Object.freeze(${JSON.stringify(this.componentState)});
${code}
let compiledCode = this.compileCode(this.props.code);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const?

@sapegin
Copy link
Member

sapegin commented Jun 22, 2016

@saschatimme Could you please update your branch and fix conflicts? I’ll merge it after that.

@sapegin
Copy link
Member

sapegin commented Jun 22, 2016

There are a few ESLint warning now.

@saschatimme
Copy link
Contributor Author

@sapegin sorry for that, it should be fixed now.

@sapegin sapegin merged commit 88de0c2 into styleguidist:master Jun 22, 2016
@sapegin
Copy link
Member

sapegin commented Jun 22, 2016

Thanks! :shipit:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants