Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,10 @@ React Patterns

[Pull request](https://github.com/nickovchinnikov/react-js-tutorial/pull/23) <br>
[Presentation](https://drive.google.com/file/d/1VezGwpkXUV38X-pL48j9ZjC58ASPuTPl/view?usp=sharing) <br>


## Lesson 16:
* React + Redux

[Pull request](https://github.com/nickovchinnikov/react-js-tutorial/pull/27) <br>
[Presentation](https://docs.google.com/presentation/d/1kng-DBHU91jQqjWHQVOMNMZjjfe8zawS8zzLP44TnuM/edit?usp=sharing) <br>
241 changes: 150 additions & 91 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@
"@types/ramda": "^0.27.6",
"@types/react": "^16.9.33",
"@types/react-dom": "^16.9.6",
"@types/react-redux": "^7.1.9",
"@types/react-router-dom": "^5.1.5",
"@types/react-test-renderer": "^16.9.2",
"@types/redux-mock-store": "^1.0.2",
"@typescript-eslint/eslint-plugin": "^2.25.0",
"@typescript-eslint/parser": "^2.25.0",
"babel-jest": "^25.2.4",
Expand All @@ -74,12 +76,13 @@
"prettier": "^2.0.2",
"react-docgen-typescript-loader": "^3.7.2",
"react-test-renderer": "^16.13.1",
"redux-mock-store": "^1.5.4",
"storybook-addon-react-docgen": "^1.2.32",
"ts-node": "^8.8.2",
"typescript": "^3.8.3",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
"webpack-dev-server": "^3.11.0"
},
"dependencies": {
"@emotion/core": "^10.0.28",
Expand All @@ -88,6 +91,7 @@
"ramda": "^0.27.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^7.2.0",
"react-router-dom": "^5.2.0",
"redux": "^4.0.5"
},
Expand Down
62 changes: 33 additions & 29 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,39 @@ import { FieldScreen } from "@/screens/FieldScreen";
import { NoMatchScreen } from "@/screens/NoMatchScreen";
import { UserScreen } from "@/screens/UserScreen";
import { ReduxScreen } from "@/screens/ReduxScreen";
import { Provider } from "react-redux";
import { store } from "@/rdx/store";

export const App: React.FC<{}> = () => (
<Router>
<nav>
<ul>
<li>
<Link to="/login">Login</Link>
</li>
<li>
<Link to="/field">Field</Link>
</li>
<li>
<Link to="/user/Nick">Nick</Link>
</li>
<li>
<Link to="/redux">Redux</Link>
</li>
</ul>
</nav>
<Switch>
<Route path="/login">
<LoginScreen />
</Route>
<Route path="/field" render={() => <FieldScreen />} />
<Route path="/user/:name" component={UserScreen} />
<Route path="/redux" component={ReduxScreen} />
<Route path="*">
<NoMatchScreen />
</Route>
</Switch>
</Router>
<Provider store={store}>
<Router>
<nav>
<ul>
<li>
<Link to="/login">Login</Link>
</li>
<li>
<Link to="/field">Field</Link>
</li>
<li>
<Link to="/user/Nick">Nick</Link>
</li>
<li>
<Link to="/redux">Redux</Link>
</li>
</ul>
</nav>
<Switch>
<Route path="/login">
<LoginScreen />
</Route>
<Route path="/field" render={() => <FieldScreen />} />
<Route path="/user/:name" component={UserScreen} />
<Route path="/redux" component={ReduxScreen} />
<Route path="*">
<NoMatchScreen />
</Route>
</Switch>
</Router>
</Provider>
);
20 changes: 13 additions & 7 deletions src/components/NextMove.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import React from 'react';
import { withRedux } from '@/utils/withRedux';
import { TicTacToeGameState } from '@/rdx/reducer';
import React from "react";
import { TicTacToeGameState } from "@/rdx/reducer";
import { connect } from "react-redux";

const RawNextMove: React.FC<{ nextMove: string }> = ({ nextMove }) => <h2>Next move is for {nextMove}</h2>;
const RawNextMove: React.FC<{ nextMove: string }> = ({ nextMove }) => (
<h2>Next move is for {nextMove}</h2>
);

export const NextMove = withRedux(RawNextMove, (state: TicTacToeGameState) => ({
nextMove: state.nextMove
}));
function mapStateToProps(state: TicTacToeGameState) {
return {
nextMove: state.nextMove,
};
}

export const NextMove = connect(mapStateToProps)(RawNextMove);
18 changes: 18 additions & 0 deletions src/rdx/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const X_MOVE = "X_MOVE";
export const O_MOVE = "O_MOVE";

export type Coordinates = { x: number; y: number };

export function xMove(payload: Coordinates) {
return {
type: X_MOVE,
payload,
};
}

export function oMove(payload: Coordinates) {
return {
type: O_MOVE,
payload,
};
}
2 changes: 1 addition & 1 deletion src/rdx/reducer/gameField.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Action } from "redux";
import * as actionTypes from '@/rdx/types';
import * as actionTypes from '@/rdx/actions';

type GameFieldState = string[][];

Expand Down
2 changes: 1 addition & 1 deletion src/rdx/reducer/nextMove.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Action } from "redux";
import * as actionTypes from '@/rdx/types';
import * as actionTypes from '@/rdx/actions';

type nextMoveState = 'x' | 'o';

Expand Down
2 changes: 0 additions & 2 deletions src/rdx/types.ts

This file was deleted.

107 changes: 107 additions & 0 deletions src/screens/ReduxScreen.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from "react";
import { ReduxScreen } from "./ReduxScreen";
import { Provider } from "react-redux";
import { mount } from "enzyme";
import { reducer } from "@/rdx/reducer";
import { createStore } from "redux";
import configureStore from "redux-mock-store";

describe("ReduxScreen with mocked store", () => {
const mockStore = configureStore([]);

let store: any;

beforeEach(() => {
store = mockStore({
nextMove: "x",
gameField: [[]],
});
});

it("should generate action on click", () => {
const wrapper = mount(
<Provider store={store}>
<ReduxScreen />
</Provider>
);

(wrapper.find("Field").props() as any).onClick(100, 999);

expect(store.getActions()).toMatchInlineSnapshot(`
Array [
Object {
"payload": Object {
"x": 100,
"y": 999,
},
"type": "X_MOVE",
},
]
`);
});
});

describe("ReduxScreen with real store", () => {
let store: any;

beforeEach(() => {
store = createStore(reducer, {
nextMove: "x",
gameField: [
["", ""],
["", ""],
],
});
jest.spyOn(store, "dispatch");
});

it("should generate action on click", () => {
const wrapper = mount(
<Provider store={store}>
<ReduxScreen />
</Provider>
);

(wrapper.find("Field").props() as any).onClick(0, 1);
wrapper.update(); // we need this if we're going to compare snapshots
(wrapper.find("Field").props() as any).onClick(1, 1);

expect(store.getState()).toMatchInlineSnapshot(`
Object {
"gameField": Array [
Array [
"",
"",
],
Array [
"x",
"o",
],
],
"nextMove": "x",
}
`);
expect((store.dispatch as jest.Mock).mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"payload": Object {
"x": 0,
"y": 1,
},
"type": "X_MOVE",
},
],
Array [
Object {
"payload": Object {
"x": 1,
"y": 1,
},
"type": "O_MOVE",
},
],
]
`);
});
});
60 changes: 32 additions & 28 deletions src/screens/ReduxScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
import React from 'react';
import * as actionTypes from '@/rdx/types';
import { Field } from '@/components/InteractiveField/components/Field';
import { withRedux } from '@/utils/withRedux';
import { Action } from 'redux';
import { NextMove } from 'components/NextMove';
import { TicTacToeGameState } from '@/rdx/reducer';

function getReduxScreenState(state: TicTacToeGameState) {
return {
gameField: state.gameField,
nextMove: state.nextMove,
};
}
import React from "react";
import { Field } from "@/components/InteractiveField/components/Field";
import { NextMove } from "components/NextMove";
import { TicTacToeGameState } from "@/rdx/reducer";
import { Coordinates, xMove, oMove } from "@/rdx/actions";
import { connect } from "react-redux";

interface RawReduxScreenProps {
nextMove: string;
gameField: string[][];
dispatch: (action: Action & { payload?: any }) => void;
xMove: (coords: Coordinates) => void;
oMove: (coords: Coordinates) => void;
}

class RawReduxScreen extends React.Component<RawReduxScreenProps, {}>{
class RawReduxScreen extends React.Component<RawReduxScreenProps, {}> {
onCellClick = (x: number, y: number) => {
this.props.dispatch({
type: this.props.nextMove === 'x' ? actionTypes.X_MOVE : actionTypes.O_MOVE,
payload: { x, y },
})
}
this.props[this.props.nextMove === "x" ? "xMove" : "oMove"]({ x, y });
};

render() {
return <div>
<h1>Open console to observe</h1>
<NextMove />
<Field field={this.props.gameField} onClick={this.onCellClick} />
<pre>{JSON.stringify(this.props, null, 2)}</pre>
</div>
return (
<div>
<h1>Open console to observe</h1>
<NextMove />
<Field field={this.props.gameField} onClick={this.onCellClick} />
<pre>{JSON.stringify(this.props, null, 2)}</pre>
</div>
);
}
}

export const ReduxScreen = withRedux(RawReduxScreen, getReduxScreenState);
function mapStateToProps(state: TicTacToeGameState) {
return {
gameField: state.gameField,
nextMove: state.nextMove,
};
}

const mapDispatchToProps = { xMove, oMove };

export const ReduxScreen = connect(
mapStateToProps,
mapDispatchToProps
)(RawReduxScreen);