Skip to content

Commit

Permalink
[RFC] Hooks-based saga and reducer injectors (react-boilerplate#2583)
Browse files Browse the repository at this point in the history
## React Boilerplate

The saga and reducer injectors are normally used via HOCs.

The HOCs will still work with Hooks-based components but IMO, we can offer a nice alternative which doesn't pollute the render tree:

`useInjectSaga` and `useInjectReducer` hooks!

You can see this alternative in this PR's diff [here](https://github.com/react-boilerplate/react-boilerplate/pull/2583/files?w=1) (with the no whitespace option enabled).

We probably shouldn't remove the HOCs as the Hooks-based injectors won't work with Class components.

I do have one question about the InjectSaga HOC: It passes props to `injectSaga`. What's the purpose of this and is there any way around it?

This is more of an RFC than anything else. Looking for thoughts and feedback before completing the branch.

Cheers!

PS: `react-helmet@5` + `useEffect` in the same component = 🧨💥🔥 so I upgraded us to the 6.0.0 beta.

![image](https://user-images.githubusercontent.com/8948127/53353299-13340a80-392d-11e9-8230-4fb0e7992191.png)
  • Loading branch information
julienben authored Apr 8, 2019
1 parent 4388ce0 commit 434a12a
Show file tree
Hide file tree
Showing 16 changed files with 280 additions and 291 deletions.
35 changes: 16 additions & 19 deletions app/components/Header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,22 @@ import HeaderLink from './HeaderLink';
import Banner from './banner.jpg';
import messages from './messages';

/* eslint-disable react/prefer-stateless-function */
class Header extends React.Component {
render() {
return (
<div>
<A href="https://twitter.com/mxstbr">
<Img src={Banner} alt="react-boilerplate - Logo" />
</A>
<NavBar>
<HeaderLink to="/">
<FormattedMessage {...messages.home} />
</HeaderLink>
<HeaderLink to="/features">
<FormattedMessage {...messages.features} />
</HeaderLink>
</NavBar>
</div>
);
}
function Header() {
return (
<div>
<A href="https://www.reactboilerplate.com/">
<Img src={Banner} alt="react-boilerplate - Logo" />
</A>
<NavBar>
<HeaderLink to="/">
<FormattedMessage {...messages.home} />
</HeaderLink>
<HeaderLink to="/features">
<FormattedMessage {...messages.features} />
</HeaderLink>
</NavBar>
</div>
);
}

export default Header;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`<Header /> should render a div 1`] = `
<div>
<a
class="A-br8h0y-0 A-p44m4v-0 cYguYL"
href="https://twitter.com/mxstbr"
href="https://www.reactboilerplate.com/"
>
<img
alt="react-boilerplate - Logo"
Expand Down
122 changes: 57 additions & 65 deletions app/containers/FeaturePage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,73 +13,65 @@ import List from './List';
import ListItem from './ListItem';
import ListItemTitle from './ListItemTitle';

export default class FeaturePage extends React.Component {
// Since state and props are static,
// there's no need to re-render this component
shouldComponentUpdate() {
return false;
}
export default function FeaturePage() {
return (
<div>
<Helmet>
<title>Feature Page</title>
<meta
name="description"
content="Feature page of React.js Boilerplate application"
/>
</Helmet>
<H1>
<FormattedMessage {...messages.header} />
</H1>
<List>
<ListItem>
<ListItemTitle>
<FormattedMessage {...messages.scaffoldingHeader} />
</ListItemTitle>
<p>
<FormattedMessage {...messages.scaffoldingMessage} />
</p>
</ListItem>

render() {
return (
<div>
<Helmet>
<title>Feature Page</title>
<meta
name="description"
content="Feature page of React.js Boilerplate application"
/>
</Helmet>
<H1>
<FormattedMessage {...messages.header} />
</H1>
<List>
<ListItem>
<ListItemTitle>
<FormattedMessage {...messages.scaffoldingHeader} />
</ListItemTitle>
<p>
<FormattedMessage {...messages.scaffoldingMessage} />
</p>
</ListItem>
<ListItem>
<ListItemTitle>
<FormattedMessage {...messages.feedbackHeader} />
</ListItemTitle>
<p>
<FormattedMessage {...messages.feedbackMessage} />
</p>
</ListItem>

<ListItem>
<ListItemTitle>
<FormattedMessage {...messages.feedbackHeader} />
</ListItemTitle>
<p>
<FormattedMessage {...messages.feedbackMessage} />
</p>
</ListItem>
<ListItem>
<ListItemTitle>
<FormattedMessage {...messages.routingHeader} />
</ListItemTitle>
<p>
<FormattedMessage {...messages.routingMessage} />
</p>
</ListItem>

<ListItem>
<ListItemTitle>
<FormattedMessage {...messages.routingHeader} />
</ListItemTitle>
<p>
<FormattedMessage {...messages.routingMessage} />
</p>
</ListItem>
<ListItem>
<ListItemTitle>
<FormattedMessage {...messages.networkHeader} />
</ListItemTitle>
<p>
<FormattedMessage {...messages.networkMessage} />
</p>
</ListItem>

<ListItem>
<ListItemTitle>
<FormattedMessage {...messages.networkHeader} />
</ListItemTitle>
<p>
<FormattedMessage {...messages.networkMessage} />
</p>
</ListItem>

<ListItem>
<ListItemTitle>
<FormattedMessage {...messages.intlHeader} />
</ListItemTitle>
<p>
<FormattedMessage {...messages.intlMessage} />
</p>
</ListItem>
</List>
</div>
);
}
<ListItem>
<ListItemTitle>
<FormattedMessage {...messages.intlHeader} />
</ListItemTitle>
<p>
<FormattedMessage {...messages.intlMessage} />
</p>
</ListItem>
</List>
</div>
);
}
20 changes: 0 additions & 20 deletions app/containers/FeaturePage/tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,4 @@ describe('<FeaturePage />', () => {

expect(firstChild).toMatchSnapshot();
});

it('should never re-render the component', () => {
const shouldComponentUpdateMock = jest.spyOn(
FeaturePage.prototype,
'shouldComponentUpdate',
);
const { rerender } = render(
<IntlProvider locale="en">
<FeaturePage />
</IntlProvider>,
);

rerender(
<IntlProvider locale="en">
<FeaturePage test="dummy" />
</IntlProvider>,
);

expect(shouldComponentUpdateMock).toHaveReturnedWith(false);
});
});
136 changes: 68 additions & 68 deletions app/containers/HomePage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
* This is the first thing users see of our App, at the '/' route
*/

import React from 'react';
import React, { useEffect, memo } from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { createStructuredSelector } from 'reselect';

import injectReducer from 'utils/injectReducer';
import injectSaga from 'utils/injectSaga';
import { useInjectReducer } from 'utils/injectReducer';
import { useInjectSaga } from 'utils/injectSaga';
import {
makeSelectRepos,
makeSelectLoading,
Expand All @@ -33,68 +33,72 @@ import { makeSelectUsername } from './selectors';
import reducer from './reducer';
import saga from './saga';

/* eslint-disable react/prefer-stateless-function */
export class HomePage extends React.PureComponent {
/**
* when initial state username is not null, submit the form to load repos
*/
componentDidMount() {
if (this.props.username && this.props.username.trim().length > 0) {
this.props.onSubmitForm();
}
}
const key = 'home';

render() {
const { loading, error, repos } = this.props;
const reposListProps = {
loading,
error,
repos,
};
export function HomePage({
username,
loading,
error,
repos,
onSubmitForm,
onChangeUsername,
}) {
useInjectReducer({ key, reducer });
useInjectSaga({ key, saga });

return (
<article>
<Helmet>
<title>Home Page</title>
<meta
name="description"
content="A React.js Boilerplate application homepage"
/>
</Helmet>
<div>
<CenteredSection>
<H2>
<FormattedMessage {...messages.startProjectHeader} />
</H2>
<p>
<FormattedMessage {...messages.startProjectMessage} />
</p>
</CenteredSection>
<Section>
<H2>
<FormattedMessage {...messages.trymeHeader} />
</H2>
<Form onSubmit={this.props.onSubmitForm}>
<label htmlFor="username">
<FormattedMessage {...messages.trymeMessage} />
<AtPrefix>
<FormattedMessage {...messages.trymeAtPrefix} />
</AtPrefix>
<Input
id="username"
type="text"
placeholder="mxstbr"
value={this.props.username}
onChange={this.props.onChangeUsername}
/>
</label>
</Form>
<ReposList {...reposListProps} />
</Section>
</div>
</article>
);
}
useEffect(() => {
// When initial state username is not null, submit the form to load repos
if (username && username.trim().length > 0) onSubmitForm();
}, []);

const reposListProps = {
loading,
error,
repos,
};

return (
<article>
<Helmet>
<title>Home Page</title>
<meta
name="description"
content="A React.js Boilerplate application homepage"
/>
</Helmet>
<div>
<CenteredSection>
<H2>
<FormattedMessage {...messages.startProjectHeader} />
</H2>
<p>
<FormattedMessage {...messages.startProjectMessage} />
</p>
</CenteredSection>
<Section>
<H2>
<FormattedMessage {...messages.trymeHeader} />
</H2>
<Form onSubmit={onSubmitForm}>
<label htmlFor="username">
<FormattedMessage {...messages.trymeMessage} />
<AtPrefix>
<FormattedMessage {...messages.trymeAtPrefix} />
</AtPrefix>
<Input
id="username"
type="text"
placeholder="mxstbr"
value={username}
onChange={onChangeUsername}
/>
</label>
</Form>
<ReposList {...reposListProps} />
</Section>
</div>
</article>
);
}

HomePage.propTypes = {
Expand Down Expand Up @@ -128,11 +132,7 @@ const withConnect = connect(
mapDispatchToProps,
);

const withReducer = injectReducer({ key: 'home', reducer });
const withSaga = injectSaga({ key: 'home', saga });

export default compose(
withReducer,
withSaga,
withConnect,
memo,
)(HomePage);
Loading

0 comments on commit 434a12a

Please sign in to comment.