18 | `;
19 |
--------------------------------------------------------------------------------
/src/components/TagsList.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TagsList from './TagsList'
3 | import renderer from 'react-test-renderer';
4 |
5 |
6 | describe("The tags list",()=>{
7 | /**
8 | * The tagsList can be tested against an expected snapshot value, as in below.
9 | */
10 | it ("renders as expected",()=>{
11 | const tree = renderer
12 | .create()
13 | .toJSON();
14 |
15 | expect(tree).toMatchSnapshot();
16 | });
17 | });
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/NotificationsViewer.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`The stateful notifications viewer Should have a passing snapshot 1`] = `
4 |
7 |
10 | Loading...
11 |
12 |
13 | `;
14 |
15 | exports[`The stateful notifications viewer Should have a passing snapshot 2`] = `
16 |
19 |
22 | 5 Notifications
23 |
24 |
25 | `;
26 |
--------------------------------------------------------------------------------
/data/api-real-url.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The URL to receive a list of questions in JSON from StackOverflow.
3 | * Works if you paste it into your browser's URL bar.
4 | * Subject to eventual deprecation by its authors (Use mock data after that point)
5 | */
6 | export const questions = `https://api.stackexchange.com/2.0/questions?site=stackoverflow`;
7 | /**
8 | * The URL to receive details on a single question.
9 | * This request also returns the body of the question
10 | * @param id
11 | * The question ID to fetch
12 | */
13 | export const question = (id)=>`https://api.stackexchange.com/2.0/questions/${id}?site=stackoverflow&filter=withbody`;
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/QuestionDetail.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`The question detail view The display component Should not regress 1`] = `
4 |
5 |
8 | The meaning of life
9 |
10 |
11 |
14 |
15 |
16 | hitchhiking
17 |
18 |
19 |
20 |
21 |
22 | 42
23 |
24 |
25 |
26 | 0
27 | Answers
28 |
29 |
30 |
31 | `;
32 |
--------------------------------------------------------------------------------
/src/components/__snapshots__/QuestionList.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`The question list The items of the question list should display a list of items 1`] = `
4 |
33 | `;
34 |
--------------------------------------------------------------------------------
/src/sagas/fetch-question-saga.js:
--------------------------------------------------------------------------------
1 | import { takeEvery, put } from 'redux-saga/effects'
2 | import fetch from 'isomorphic-fetch';
3 |
4 | export default function * () {
5 | /**
6 | * Every time REQUEST_FETCH_QUESTION, fork a handleFetchQuestion process for it
7 | */
8 | yield takeEvery(`REQUEST_FETCH_QUESTION`,handleFetchQuestion);
9 | }
10 |
11 | /**
12 | * Fetch question details from the local proxy API
13 | */
14 | export function * handleFetchQuestion ({question_id}) {
15 | const raw = yield fetch(`/api/questions/${question_id}`);
16 | const json = yield raw.json();
17 | const question = json.items[0];
18 | /**
19 | * Notify application that question has been fetched
20 | */
21 | yield put({type:`FETCHED_QUESTION`,question});
22 | }
--------------------------------------------------------------------------------
/src/sagas/fetch-questions-saga.js:
--------------------------------------------------------------------------------
1 | import { put, take } from 'redux-saga/effects'
2 | import fetch from 'isomorphic-fetch';
3 | /**
4 | * Fetch questions saga gets a list of all new
5 | * questions in response to a particular view being loaded
6 | */
7 | export default function * () {
8 | while (true) {
9 | /**
10 | * Wait for a request to fetch questions, then fetch data from the API and notify the application
11 | * that new questions have been loaded.
12 | */
13 | yield take(`REQUEST_FETCH_QUESTIONS`);
14 | console.log("Got fetch questions request");
15 | const raw = yield fetch('/api/questions');
16 | const json = yield raw.json();
17 | const questions = json.items;
18 | yield put({type:`FETCHED_QUESTIONS`,questions});
19 | }
20 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Isomorphic React
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | <%= preloadedApplication %>
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/reducers/questions.js:
--------------------------------------------------------------------------------
1 | import unionWith from 'lodash/unionWith';
2 |
3 | /**
4 | * Questions reducer, deals mostly with actions dispatched from sagas.
5 | */
6 | export const questions = (state = [],{type,question,questions})=>{
7 | /**
8 | * Question Equality returns true if two questions are equal, based on a weak check of their question_id property
9 | * @param a
10 | * The first question
11 | * @param b
12 | * The second question
13 | * @returns {boolean}
14 | * Whether the questions are equal
15 | */
16 | const questionEquality = (a = {},b = {})=>{
17 | return a.question_id == b.question_id
18 | };
19 |
20 | /**
21 | * Create a new state by combining the existing state with the question(s) that has been newly fetched
22 | */
23 | if (type === `FETCHED_QUESTION`) {
24 | state = unionWith([question],state,questionEquality);
25 | }
26 |
27 | if (type === `FETCHED_QUESTIONS`) {
28 | state = unionWith(state,questions,questionEquality);
29 | }
30 | return state;
31 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 | .idea
14 |
15 | public/bundle.js
16 | # Directory for instrumented libs generated by jscoverage/JSCover
17 | lib-cov
18 |
19 | # Coverage directory used by tools like istanbul
20 | coverage
21 |
22 | # nyc test coverage
23 | .nyc_output
24 |
25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
26 | .grunt
27 |
28 | # Bower dependency directory (https://bower.io/)
29 | bower_components
30 |
31 | # node-waf configuration
32 | .lock-wscript
33 |
34 | # Compiled binary addons (https://nodejs.org/api/addons.html)
35 | build/Release
36 |
37 | # Dependency directories
38 | node_modules/
39 | jspm_packages/
40 |
41 | # Typescript v1 declaration files
42 | typings/
43 |
44 | # Optional npm cache directory
45 | .npm
46 |
47 | # Optional eslint cache
48 | .eslintcache
49 |
50 | # Optional REPL history
51 | .node_repl_history
52 |
53 | dist
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 |
63 |
--------------------------------------------------------------------------------
/src/components/NotificationsViewer.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import delay from 'redux-saga';
4 |
5 | import Adapter from 'enzyme-adapter-react-16';
6 | import { shallow, configure } from 'enzyme';
7 |
8 | configure({adapter: new Adapter()});
9 |
10 | import NotificationsViewer from './NotificationsViewer';
11 |
12 | jest.mock('../services/NotificationsService');
13 |
14 | const notificationService = require('../services/NotificationsService').default;
15 |
16 | notificationService.default = jest.fn();
17 |
18 | describe('The notification viewer', () => {
19 |
20 | beforeAll(() => {
21 | notificationService.default.mockClear();
22 | notificationService.__setCount(42);
23 | });
24 |
25 | it('should display the correct number of notifications', async() => {
26 | const tree = renderer.create();
27 | const wrapper = shallow();
28 |
29 | await delay();
30 |
31 | const instance = tree.root;
32 |
33 | await wrapper.instance().componentDidMount();
34 |
35 | const component = instance.findByProps({className: `notifications`});
36 | const text = component.children[0];
37 |
38 | expect(text).toEqual('42 Notifications');
39 | });
40 | })
41 |
--------------------------------------------------------------------------------
/src/sagas/fetch-question-saga.spec.js:
--------------------------------------------------------------------------------
1 | import { handleFetchQuestion } from './fetch-question-saga';
2 | import fetch from 'isomorphic-fetch';
3 | /**
4 | * This test is an example of two important Jest testing principles,
5 | * 1) we're mocking the "fetch" module, so that we don't actually make a request every time we run the test
6 | * The module, isomorphic fetch, is conveniently mocked automatically be including the file __mocks__/isomorphic-fetch.js adjacent to to the Node.js folder
7 | * 2) we're using an async function to automatically deal with the fact that our app isn't synchronous
8 | */
9 | describe("Fetch questions saga",()=>{
10 | beforeAll(()=>{
11 | fetch.__setValue([{question_id:42}]);
12 | });
13 | it("should get the questions from the correct endpoint in response to the appropriate action",async ()=>{
14 | const gen = handleFetchQuestion({question_id:42});
15 | /**
16 | * At this point, isomorphic fetch must have been mocked,
17 | * or an error will occur, or, worse, an unexpected side effect!
18 | */
19 | const { value } = await gen.next();
20 | expect(value).toEqual([{question_id:42}]);
21 |
22 | /**
23 | * We can also assert that fetch has been called with the values expected (note that we used a spy in the file where we mock fetch.)
24 | */
25 | expect(fetch).toHaveBeenCalledWith(`/api/questions/42`);
26 | });
27 | });
--------------------------------------------------------------------------------
/src/components/__tests__/NotificationsViewer.js:
--------------------------------------------------------------------------------
1 | import NotificationsViewer from '../NotificationsViewer'
2 | import renderer from 'react-test-renderer';
3 | import React from 'react';
4 | import delay from 'redux-saga';
5 |
6 | /**
7 | * Locally created modules must be explicitly
8 | * mocked (unlike NPM modules, which need only that the mock file exists
9 | */
10 | jest.mock('../../services/NotificationsService');
11 |
12 | /**
13 | * This require statement now imports the mock service. We will use this reference to inject a return value.
14 | */
15 | const notificationsService = require('../../services/NotificationsService').default;
16 |
17 | describe("The stateful notifications viewer",()=>{
18 |
19 | beforeAll(()=>{
20 | notificationsService.__setCount(5);
21 | });
22 |
23 | it("Should display the correct number of notifications", async ()=>{
24 | const tree = renderer
25 | .create(
26 |
27 | );
28 |
29 | await delay();
30 | const instance = tree.root;
31 | const component = instance.findByProps({className:`notifications`});
32 | const text = component.children[0];
33 | expect(text).toEqual("5 Notifications");
34 | });
35 |
36 | it("Should have a passing snapshot", async ()=>{
37 | const tree = renderer.create();
38 | expect(tree).toMatchSnapshot();
39 | await delay();
40 | expect(tree).toMatchSnapshot();
41 | })
42 | });
--------------------------------------------------------------------------------
/webpack.config.prod.babel.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TerserPlugin = require("terser-webpack-plugin");
3 | const webpack = require('webpack');
4 |
5 | /**
6 | * Production Webpack Config bundles JS, then uglifies it and exports it to the "dist" directory
7 | * See Development webpack config for detailed comments
8 | */
9 |
10 | module.exports = {
11 | mode: 'development',
12 | entry: [
13 | 'babel-regenerator-runtime',
14 | path.resolve(__dirname, 'src')
15 | ],
16 | output: {
17 | path: path.resolve(__dirname, 'dist'),
18 | filename: 'bundle.js',
19 | publicPath: '/'
20 | },
21 | optimization: {
22 | minimize: true,
23 | minimizer: [new TerserPlugin()],
24 | },
25 | plugins: [
26 | new webpack.DefinePlugin({
27 | 'process.env': {
28 | NODE_ENV: JSON.stringify('production'),
29 | WEBPACK: true
30 | }
31 | }),
32 | /*
33 | * Uglifies JS which improves performance
34 | * React will throw console warnings if this is not implemented
35 | */
36 | ],
37 | resolve: {
38 | extensions: ['.js', '.json', '.jsx'],
39 | },
40 | module: {
41 | rules: [
42 | {
43 | test: /.jsx?$/,
44 | use: {
45 | loader: 'babel-loader'
46 | },
47 | include: path.resolve(__dirname, 'src')
48 | }
49 | ]
50 | }
51 | };
--------------------------------------------------------------------------------
/src/reducers/questions.spec.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Placing every test inside a "describe" block is not a requirement but makes for a good, clean means of organization.
3 | */
4 | import { questions } from './questions';
5 |
6 | /**
7 | * Testing Reducers is the easiest part of a a React / Redux application.
8 | * Reducers are pure functions, so they never depend on dependencies that need to be mocked, and
9 | * can be relied upon to return the same data given similar arguments.
10 | */
11 | describe("The questions reducer",()=>{
12 | it ("Should return the same state on an inapplicable action",()=>{
13 | const state = ["foo","bar"];
14 | const newState = questions(state,{type:"UNDEFINED_ACTION"});
15 |
16 | /**
17 | * In this case both toBe and toEqual will return true. However, the real test in this example is toBe, since we want literally the same object both times.
18 | *
19 | */
20 | expect(newState).toBe(state);
21 | expect(newState).toEqual(state);
22 | });
23 |
24 | it ("Should add a new questions to the list on a FETCHED_QUESTION action",()=>{
25 | const state = [{question_id:"foo"},{question_id:"bar"}];
26 | const newState = questions(state,{type:`FETCHED_QUESTION`,question:{question_id:"baz"}});
27 |
28 | /** Here a map is used to allow a simple one-liner as an equality check.
29 | * TODO... is there a more applicable matcher available?
30 | */
31 | expect(newState.map(q=>q.question_id))
32 | .toEqual(["baz","foo","bar"]);
33 | });
34 | });
--------------------------------------------------------------------------------
/src/components/__tests__/QuestionDetail.js:
--------------------------------------------------------------------------------
1 | /**
2 | * In this file we explore testing a React Redux component by writing seperate tests for the display and the container
3 | */
4 | import { QuestionDetailDisplay , mapStateToProps } from '../QuestionDetail'
5 | import renderer from 'react-test-renderer';
6 | import React from 'react';
7 |
8 | describe("The question detail view",()=>{
9 | describe("The display component",()=>{
10 | it("Should not regress",()=>{
11 | const tree = renderer
12 | .create(
13 |
19 | );
20 | expect(tree.toJSON()).toMatchSnapshot();
21 |
22 | }) ;
23 | });
24 |
25 | /**
26 | * Note that since Map State to Props is a pure function, we can easily test it here
27 | * without needing to scaffold the entire application around it.
28 | */
29 | describe("Map state to props",()=>{
30 | const sampleQuestion = {
31 | question_id:42,
32 | body:"Space is big"
33 | };
34 |
35 | const appState = {
36 | questions:[sampleQuestion]
37 | };
38 |
39 | const ownProps = {
40 | question_id: 42
41 | };
42 |
43 | const componentState = mapStateToProps(appState,ownProps);
44 |
45 | expect(componentState).toEqual(sampleQuestion);
46 |
47 | });
48 | });
--------------------------------------------------------------------------------
/src/components/QuestionDetail.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Markdown from 'react-markdown';
3 | import TagsList from './TagsList'
4 | import { connect } from 'react-redux';
5 |
6 | /**
7 | * Question Detail Display outputs a view containing question information when passed a question
8 | * as its prop
9 | * If no question is found, that means the saga that is loading it has not completed, and display an interim message
10 | */
11 | export const QuestionDetailDisplay = ({title,body,answer_count,tags})=>(
12 |
13 |
14 | {title}
15 |
16 | {body ?
17 |
18 |
19 |
20 |
21 |
22 |
23 | {answer_count} Answers
24 |
25 |
:
26 |
27 | {/* If saga has not yet gotten question details, display loading message instead. */}
28 |
29 | Loading Question...
30 |
31 |
32 | }
33 |
34 | );
35 |
36 | export const mapStateToProps = (state,ownProps)=>({
37 | /**
38 | * Find the question in the state that matches the ID provided and pass it to the display component
39 | */
40 | ...state.questions.find(({question_id})=>question_id == ownProps.question_id)
41 | });
42 |
43 | /**
44 | * Create and export a connected component
45 | */
46 | export default connect(mapStateToProps)(QuestionDetailDisplay);
--------------------------------------------------------------------------------
/src/components/QuestionList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TagsList from './TagsList'
3 | import { connect } from 'react-redux';
4 | import {
5 | Link
6 | } from 'react-router-dom'
7 |
8 | /**
9 | * Each entry in the QuestionList is represtented by a QuestionListItem, which displays high-level information
10 | * about a question in a format that works well in a list
11 | */
12 | export const QuestionListItem = ({tags,answer_count,title,views,question_id})=>(
13 |
14 |
15 | {title}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
);
26 |
27 | /**
28 | * Display all questions in an array provided to it as a simple list
29 | */
30 | export const QuestionList = ({questions})=>(
31 |
32 | { questions ?
33 |
34 | {questions.map(question=>)}
35 |
:
36 |
37 | Loading questions...
38 |
39 | }
40 |
41 | );
42 |
43 | /**
44 | * Get the list of questions from the application's state
45 | * It is populated by a ../sagas/fetch-question(s)-saga.
46 | */
47 | const mapStateToProps = ({questions})=>({
48 | questions
49 | });
50 |
51 | /**
52 | * Create and export a connected component
53 | */
54 | export default connect(mapStateToProps)(QuestionList);
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import QuestionList from './components/QuestionList'
4 | import QuestionDetail from './components/QuestionDetail'
5 | import NotificationsViewer from './components/NotificationsViewer'
6 |
7 | import { connect } from 'react-redux';
8 | import { Route, Link } from 'react-router-dom';
9 |
10 | /**
11 | * App Component is the highest level real component in the application, it is the parent of the routes and an
12 | * an ancestors of all other compoents
13 | */
14 | const AppDisplay = ()=>(
15 |
16 |
17 |
18 |
Isomorphic React
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {/*Specify a route for the main page which renders when the path is empty*/}
28 | }/>
29 |
30 | {/*Specify a route for questions where the detail renders differently depending on the question selected, the ID of which is passed in at render time*/}
31 | {/*It would be possible to read the current path from within the component during rendering, but this way all data is passed in through props.*/}
32 | }/>
33 |
34 | );
35 |
36 | const mapStateToProps = (state,ownProps)=>({
37 | ...state,
38 | });
39 |
40 | /**
41 | * The connected component exported below forms the
42 | * core of our application and is used both on the server and the client
43 | */
44 | export default connect(mapStateToProps)(AppDisplay);
45 |
--------------------------------------------------------------------------------
/webpack.config.dev.babel.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
3 | import path from 'path';
4 |
5 | /**
6 | * Development webpack config designed to be loaded by express development server
7 | */
8 |
9 | module.exports = {
10 | /**
11 | * The scripts in entry are combined in order to create our bundle
12 | */
13 | mode: 'development',
14 | entry: [
15 | 'babel-regenerator-runtime',
16 | path.resolve(__dirname, 'src')
17 | ],
18 | output: {
19 | path: path.resolve(__dirname, 'dist'),
20 | filename: 'bundle.js',
21 | publicPath: '/'
22 | },
23 | optimization: {
24 | minimizer: [
25 | // we specify a custom UglifyJsPlugin here to get source maps in production
26 | new UglifyJsPlugin({
27 | cache: true,
28 | parallel: true,
29 | uglifyOptions: {
30 | compress: false,
31 | ecma: 6,
32 | mangle: true
33 | },
34 | sourceMap: true
35 | })
36 | ]
37 | },
38 | plugins: [
39 | new webpack.DefinePlugin({
40 | 'process.env': {
41 | NODE_ENV: JSON.stringify('production'),
42 | WEBPACK: true
43 | }
44 | }),
45 | /*
46 | * Uglifies JS which improves performance
47 | * React will throw console warnings if this is not implemented
48 | */
49 | ],
50 | resolve: {
51 | extensions: ['.js', '.json', '.jsx'],
52 | },
53 | module: {
54 | rules: [
55 | {
56 | test: /.jsx?$/,
57 | use: {
58 | loader: 'babel-loader'
59 | },
60 | include: path.resolve(__dirname, 'src')
61 | }
62 | ]
63 | }
64 | };
--------------------------------------------------------------------------------
/src/components/QuestionList.spec.js:
--------------------------------------------------------------------------------
1 | /**
2 | * In this file we explore testing a React-Redux component by wrapping it in a MemoryRouter and checking the results
3 | */
4 |
5 | import React from 'react';
6 | import { QuestionListItem } from './QuestionList'
7 | import renderer from 'react-test-renderer';
8 | import { MemoryRouter } from 'react-router';
9 |
10 |
11 | /**
12 | * Here we test a React-Redux component. The key takeaway is that it is easier to test the decoupled "display"
13 | component and consider the "connect" component as an implementation detail.
14 | */
15 | describe("The question list",()=>{
16 | describe("The items of the question list",()=>{
17 | it ("should display a list of items",()=>{
18 | /**
19 | * Here we can run into a problem - since the component contains a link, it will throw an error by default unless
20 | * this component is nested in a router.
21 | * As a solution, the React Router GitHub page suggest wrapping the component in a memory router.
22 | * https://github.com/ReactTraining/react-router/issues/4795
23 | * This actually makes sense since any automatic solution would be too unpredictable.
24 | */
25 | const tree = renderer
26 | .create(
27 |
28 |
33 |
34 | );
35 |
36 | /**
37 | * Here we are using the basic functionality of the React Test Renderer to run assertions against
38 | * the rendered HTML (and thus avoiding additional libraries such as Enzyme)
39 | */
40 | expect(tree.root.findByType("h3").children[0]).toEqual("The meaning of life");
41 | expect(tree.toJSON()).toMatchSnapshot();
42 | });
43 |
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/src/components/NotificationsViewer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | /*
3 |
4 | import NotificationsService from '../services/NotificationsService';
5 |
6 | export default class NotificationsViewer extends React.Component {
7 | constructor(...args) {
8 | super(...args);
9 |
10 | this.state = {
11 | count: -1
12 | }
13 | }
14 |
15 | async componentDidMount () {
16 | let { count } = await NotificationsService.GetNotifications();
17 | console.log('componentDidMount count:', count);
18 |
19 | this.setState({
20 | count
21 | });
22 | }
23 |
24 | componentDidUpdate() {
25 | console.log('componentDidUpdate count:', this.state.count);
26 | }
27 |
28 | render() {
29 | return (
30 |
35 | )
36 | }
37 | }
38 | */
39 | import NotificationsService from '../services/NotificationsService'
40 |
41 | /**
42 | * The following demo class was written in a stateful manner, using the `extends` syntax, for the purpose
43 | * of demonstrating how to test such a class. It is not functionally complete and not universal.
44 | * The structure of this component is NOT recommended, please use React-Redux instead.
45 | */
46 |
47 | export default class extends React.Component {
48 | constructor(...args) {
49 | super(...args);
50 | this.state = {
51 | count: -1,
52 | }
53 | }
54 | async componentDidMount() {
55 | let {count} = await NotificationsService.getNotifications();
56 | this.setState({
57 | count,
58 | });
59 | this.state.count = count;
60 | }
61 | render() {
62 | return (
63 |
64 |