スキップしてメイン コンテンツに移動

[TypeScript] Try React + Redux

Intro

[TypeScript] Try React 1 This time, I tried Redux to manage props and states.

Manage by React

First, I tried manging props and states by React.

Manage in child components(Square.tsx)

If only in the child components(Square.tsx) used the states, I could let them manage the states.

SquareValue.ts


export type SquareValue = '◯'|'✕'|null;

Square.tsx


import React from "react";
import './Square.css';
import { SquareValue } from './SquareValue';

export type SquareProps = {
    key: number,
    value: SquareValue,
    onClick: () => void,
};
export type SquareState = {
    value: SquareValue,
}
export class Square extends React.Component<SquareProps, SquareState> {
    render() {
        return (<button className="square"
            onClick={() => this.setState({ value: '✕'})}>
            {this.state?.value}
        </button>);
    };
}

When I clicked the buttons, they would show '✕'.

Manage in the parent component(Board.tsx)

If the parent component(Board.tsx) also needed the states? It could create a state that had the child states.

Board.tsx


import React from "react";
import { Square } from "./Square";
import './Board.css';
import { SquareValue } from './SquareValue';

export type BoardState = {
    nextIsX: boolean,
    // child states
    squares: Array<SquareValue>
};
export class Board extends React.Component<any, BoardState> {
    constructor(props: any){
        super(props);
        this.state = {
            nextIsX: true,
            squares: Array(9).fill(null),
        };
    }
    public render(): JSX.Element {
        return (
            <div className="board-row">
                {Array.from(Array(9).keys())
                    .map(i => this.renderSquare(i))}
            </div>
        );
    }
    private renderSquare(i: number): JSX.Element {
        return (<Square key={i}
            value={this.state.squares[i]}
            onClick={() => this.handleClick(i)}>
            </Square>);
    }
    private handleClick(i: number) {
        const newSquares = this.state.squares.slice();
        newSquares[i] = (this.state.nextIsX)? '✕': '◯';
        const newState = {
            nextIsX: ! this.state.nextIsX,
            squares: newSquares 
        };
        this.setState(newState);
    }
}

Square.tsx


...
export class Square extends React.Component<SquareProps> {
    render() {
        return (<button className="square"
            onClick={() => this.props.onClick()}>
            {this.props.value}
        </button>);
    };
}

Because the parent component managed the child states, the child components could be changed function.

Square.tsx


...
export function renderSquare(props: SquareProps): JSX.Element {
    return (<button key={props.key} className="square"
            onClick={() => props.onClick()}>
            {props.value}
        </button>);
}

Board.tsx


...
    private renderSquare(i: number): JSX.Element {
        return (renderSquare({
            key: i,
            value: this.state.squares[i],
            onClick: () => this.handleClick(i)
        }));
    }
...

Tutorial: Intro to React – React There were some problems. One was the parent component had many responsibilities. It had to control child components, props and states. To manage their states, I tried to use Redux.

Use Redux

Redux - A predictable state container for JavaScript apps. | Redux React Redux · Official React bindings for Redux

Install

Because create-react-app could only create a React + TypeScript project or a React + Redux project. So I added Redux manually.
npm install --save redux react-redux
npm install --save-dev @types/react-redux
Following Basic Tutorial, I added Actions, ActionCreator, Reducer, and Store(connect). Basic Tutorial: Intro | Redux

Actions & ActionCreator

Actions | Redux First, I defined "What's happened?". In this sample, I defined "Clicking a Square button".

click-action.ts


export interface ClickAction {
    type: 'CLICK_SQUARE';
    id: number;
}

And I added an ActionCreator. It created Action as same as factory methods.

click-action.ts


...
export function clickSquare(id: number): ClickAction {
    return {
        type: 'CLICK_SQUARE',
        id: id
    };
}

Reducer

I added Reducer. It decided how was the state changed by Action. Reducers | Redux

board-state-reducer.ts


import { BoardState } from "./Board";
import { ClickAction } from "./click-action";

/** if the state was null, set default value */
function initialState(): BoardState {
    return {
        nextIsX: true,
        squares: Array(9).fill(null),
    };
}
export function updateSquare(state: BoardState = initialState(), action: ClickAction): BoardState {
    if (state == null || action == null) {
        return state;
    }
    if (action.type != 'CLICK_SQUARE') {
        return state;
    }
    const squares = state.squares.slice();
    squares[action.id] = (state.nextIsX)? '✕': '◯';
    return {
        nextIsX: ! state.nextIsX,
        squares
    };
}

Store

Next, I tried adding a Store. It managed the state. Store | Redux To use Redux in components, I used react-redux. Usage with React | Redux React Redux · Official React bindings for Redux In react-redux, Store was created in index.tsx and set into Provide.

index.tsx


import 'raf/polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { createStore } from 'redux';
import { updateSquare } from './tic-tac-toe/board-state-reducer';
import { Provider } from 'react-redux';

// set Reducers as argument of createStore.
const store = createStore(updateSquare);
ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);
serviceWorker.unregister();

In this sample, I set only one Reducer as argument of createStore. But I also could multiple Reducers.

connect

For using the state in components, I had to use "connect".

Board.tsx


import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
import React, { Dispatch } from "react";
import { renderSquare } from "./Square";
import './Board.css';
import { clickSquare, ClickAction } from './click-action';
import { connect } from 'react-redux';
import { SquareValue } from './SquareValue';

export type BoardState = {
    nextIsX: boolean,
    squares: Array<SquareValue>
};
/** return state what was the state of this component */
function mapStateToProps(state: BoardState): BoardState {
    return state;
}
/** return changing the state Actions */
function mapDispatchToProps(dispatch: Dispatch<ClickAction>) {
    return {
        onClick: (id: number) => dispatch(clickSquare(id))
    };
}
type Props = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>;
class Board extends React.Component<Props, BoardState> {
    public render(): JSX.Element {
        return (
            <div className="board-row">
                {Array.from(Array(9).keys())
                    .map(i => this.renderSquare(i))}
            </div>
        );
    }
    private renderSquare(i: number): JSX.Element {
        return (renderSquare({
            key: i,
            value: this.props.squares[i],
            onClick: () => this.handleClick(i)
        }))
    }
    private handleClick(i: number) {
        if (this.props.squares[i] != null) {
            return;
        }
        this.props.onClick(i);
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Board);

Although the parent component(App.tsx) use "<Board></Board>", this component had to export connect. not the component class. Todo App with Redux - CodeSandbox

コメント

このブログの人気の投稿

[Angular][ASP.NET Core] Upload chunked files

Intro I wanted to send files to Web application (made by ASP.NET Core). If the file size had been small, I didn't need do any special things. But when I tried to send a large file, the error was occurred by ASP.NET Core's limitation. Though I could change the settings, but I didn't want to do that, because I hadn't known the file sizes what would been actually using. So I splitted the data into chunks first, and sent them. After receiving all chunks, I merged them into one file. There might be some libraries or APIs (ex. Stream API) what did them automatically, but I couldn't find them. What I did [ASP.NET Core] Make CORS enabled [Angular] Split a large file into chunks [Angular][ASP.NET Core] Send and receive data as form data [ASP.NET Core] Merge chunks into one file [ASP.NET Core] Make CORS enabled Because the client side application(Angular) and the server side application(ASP.NET Core) had been separated, I had to make CORS(Cross-Origin Requests) ...

[Nest.js] Show static files

Intro I wanted to use Nest.js and WebRTC(node-webrtc). NestJS - A progressive Node.js framework Documentation | NestJS - A progressive Node.js framework And because I wanted to try with simple page(not use JavaScript frameworks), I added static HTML, CSS, JavaScript into a Nest.js project. Prepare Install First, I installed @nestjs/cli. First steps | NestJS - A progressive Node.js framework As same as last time , I couldn't do global install because I had used Volta. But I could installed by volta. volta install @nestjs/cli Create project nest new nest-web-rtc-sample volta pin node@12 Run npm start After doing "npm start", I could getting "Hello World!" from http://localhost:3000. Add static files I could add static files by two ways. @nestjs/serve-static First one of them was using "serve-static". Serve Static | NestJS - A progressive Node.js framework npm install --save @nestjs/serve-static And I needed adding a module into app.modu...

[Angular] Sending file with Observable and showing loading screen

Intro When I tried sending file data on last time, I had confused with "promise.then", "async/await" and "Observable". [Angular][ASP.NET Core] Upload chunked files So I wanted to distinct them, and this time, I tried to use "Observable" because HttpClient return Observable<any>. Call observables in order I sended file in these steps. Read file by FileReader Create directory for saving chunks send and saving chunks merge chunks to one file and delete chunks Each steps used the former steps result. So I could write by Promise.then like below. this.executeStep1() // return Promise<UploadResult> .then(result => this.executeStep2(result)) // return Promise<UploadResult> .then(result => this.executeStep3(result)) // return Promise<UploadResult> .catch(reason => console.log(reason)); Result I could write with pipe & flatMap. file-uploader.service.ts public upload(file: File): Observable<U...