TypeScript React Starter

This quick start guide will show you how to connect TypeScript and React. At the end of your study, you will have:

  • A project that uses both React and TypeScript
  • Use TSLint to review code
  • Use Jest and Enzyme to test, as well
  • Manage state through Redux

We will use the create-React-app tool to quickly create projects.

We assume that you already use Node.js and NPM. You should also have some basic knowledge of React.

Install the create – react – app

We’re going to use create-react-app because it’s a React project, with some useful tools and specification defaults. This is just a command line tool to support new React projects.

npm install -g create-react-appCopy the code

Create your new project

We will create a new project called my-app:

create-react-app my-app --scripts-version=react-scripts-tsCopy the code

React-scripts-ts can be thought of as a plug-in that introduces TypeScript into the standard creation-react-app project pipeline.

Your project layout should now look something like this:

my-app/

├─ .gitignore

├ ─ node_modules /

├ ─ public /

├ ─ SRC /

│  └─ ...

├ ─ package. Json

├ ─ tsconfig. Json

└ ─ tslint. JsonCopy the code

Note:

  • tsconfig.jsonIncludes typescript-specific configuration options for our current project.
  • tslint.jsonSave the Settings of the detection tool,TSLint, will be used.
  • package.jsonContains our dependencies, as well as some we might use to test, preview, buildappUse the shortcut key command.
  • publicIncluding some images that we are planning to deployHTMLSuch static resources, or images. In this folder, exceptindex.htmlThis file, everything else can be deleted.
  • srcContains our TypeScript and CSS code.index.tsxIs amandatoryEntry file.

Run the project

Running this program is not as easy as running.

npm run startCopy the code

This will run the start script we specified in package.json, which will hatch a service to reload the interface (hot loading) when we save the file. Normally the service runs at http://localhost:3000, but should be turned on automatically for you.

The tight polling allows us to quickly preview changes.

Test project

A test is just a command:

npm run testCopy the code

This command runs Jest, a very useful testing tool, for all files whose extensions end in.test.ts or.spec.ts. Just like running the NPM run start command, Jest will automatically run as soon as it detects a change. If you like, you can run NPM Run Start and NPM Run test at the same time, so you can preview changes while testing them.

Create a production version

When we ran the project with NPM Run Start, we didn’t do package optimization. Typically, we want the code we send to customers to be short and snappy. Optimizations like Minification can do this, but it takes more time. We call this a build for production (as opposed to a build for development).

To run a build for a production environment, simply execute the following command:

npm run buildCopy the code

This will create an optimized JS and CSS build under./build/static/js and./build/static/ CSS respectively.

Most of the time you don’t need to run the production version, which is usually useful if you need to know how big the packaged app is.

Creating a component

We’re going to write a Hello component. The component will accept a name we want to welcome (let’s call it name) and an optional number of exclamation points, making some trailing tags (enthusiasmLevel).

We wrote something like
and the component will render something like this

Hello Daniel!!

. If the enthusiasmLevel is not specified, the component will display an exclamation mark by default. If the enthusiasmLevel is 0 or negative, it will throw an error.

We’ll write a hello.tsx file:

// src/components/Hello.tsx import * as React from 'react'; export interface Props { name: string; enthusiasmLevel? : number; } function Hello({ name, enthusiasmLevel = 1 }: Props) { if (enthusiasmLevel <= 0) { throw new Error('You could be a little more enthusiastic. :D'); } return ( <div className="hello"> <div className="greeting"> Hello {name + getExclamationMarks(enthusiasmLevel)} </div>  </div> ); } export default Hello; // helpers function getExclamationMarks(numChars: number) { return Array(numChars + 1).join('! '); }Copy the code

Note that we define an interface named Props to specify the properties the component will receive. Name is required to be a string, and the enthusiasmLevel is an optional number type (you can enthusiasmLevel from? We write after its name).

We write Hello as a stateless function component (a Stateless function Component for short). Specifically, Hello is a function that takes an object named Props and deconstructs it. If our Props object does not provide the enthusiasmLevel property, it will default to 1.

Writing components through functions is one of the two main ways React allows us to create components. We can also write it as a class, if you like, as follows:

class Hello extends React.Component<Props, object> {
  render() {
    const { name, enthusiasmLevel = 1 } = this.props;

    if (enthusiasmLevel <= 0) {
      throw new Error('You could be a little more enthusiastic. :D');
    }

    return (
      <div className="hello">
        <div className="greeting">
          Hello {name + getExclamationMarks(enthusiasmLevel)}
        </div>
      </div>
    );
  }
}Copy the code

Classes are very effective when your component contains state. But we really don’t need to worry about state in this example – in fact, we specified it as an object type, in React.Component , so it’s often more refined to write an SFC. Local component state is more useful at a real-world level when creating common UI elements that can be shared between libraries. In the life cycle of our application, we’ll revisit how the application manages general state through Redux. ,>

Now that we’ve written our component, let’s dive into index.tsx and use
replaces the render method of the
component.

First, we’ll introduce it at the top of the file:

import Hello from './components/Hello';Copy the code

Then change our render call:

ReactDOM.render(
  <Hello name="TypeScript" enthusiasmLevel={10} / >.document.getElementById('root') as HTMLElement
);Copy the code

Types of assertions

The last thing we’ll point out in this section is this line document.getelementById (‘root’) as HTMLElement. This notation is a call to type assertion, sometimes called cast. It’s a useful way to tell TypeScript when you know the true type of an expression better than type detection.

In this case, we need the reason for this is the return type of the getElementById HTMLElement | null. Simply put, getElementById returns null if the element cannot be found by the given ID. We assume that getElementById is always successful, so we need to convince TypeScript of this by using the AS syntax.

TypeScript also has a trailing “bang” syntax (!) , remove null and undefined from the previous expression. So we could also write document.getelementById (‘root’)! But in this case, we want to be more specific.

Add styles 😎

Styling components using our Settings is simple. To decorate our Hello component, we can create a CSS file in the SRC/Components/hello.css directory.

.hello {
  text-align: center;
  margin: 20px;
  font-size: 48px;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

.hello button {
  margin-left: 25px;
  margin-right: 25px;
  font-size: 40px;
  min-width: 50px;
}Copy the code

The tools used by create-React-app (namely, Webpack and various loaders) allow us to import only the stylesheets we are interested in. When our build runs, any imported.css files will be concatenated into an output file. So, in SRC /components/ hello.tsx, we will add the following imports.

import './Hello.css';Copy the code

Pass the Jest writing test

We have certain assumptions about the Hello component. Let’s reiterate what they are:

  • When we write it like this<Hello name="Daniel" enthusiasmLevel={3} />Components render things like<div>Hello Daniel!!! </div>.
  • ifenthusiasmLevelIf not specified, the component should render an exclamation point.
  • ifenthusiasmLevel0Or negative, it will throw an error.

We can use these requirements to write some tests for our component.

But first, let’s install the Enzyme. Enzyme is a common tool in the React ecosystem that makes it easier to write tests that predict the behavior of components. By default, our application includes a library called JsDOM that allows us to emulate the DOM and test its runtime behavior without a browser. Enzyme is similar, but building on jsDOM makes it easier to perform certain queries on our component.

Let’s treat it as a development-time dependency installation.

npm install -D enzyme @types/enzyme react-addons-test-utilsCopy the code

The @types/enzyme package is the package that contains the actual running JavaScript code, and the @types/enzyme package is the package that contains the declaration files (.d.ts files). Make it easier for TypeScript to learn how to use enzymes. You can learn more about the @Types package here.

We also need to install react-addons-test-utils. This is what the enzyme needs.

Now that we’ve set up the Enzyme, let’s start writing tests! Let’s create a called SRC/components/hello.html test. The TSX’s file, and we created earlier. Hello TSX files in the same directory.

// src/components/Hello.test.tsx

import * as React from 'react';
import * as enzyme from 'enzyme';
import Hello from './Hello';

it('renders the correct text when no enthusiasm level is given'.(a)= > {
  const hello = enzyme.shallow(<Hello name='Daniel'/ >); expect(hello.find(".greeting").text()).toEqual('Hello Daniel! ')}); it('renders the correct text with an explicit enthusiasm of 1'.(a)= > {
  const hello = enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={1} / >); expect(hello.find(".greeting").text()).toEqual('Hello Daniel! ')}); it('renders the correct text with an explicit enthusiasm level of 5'.(a)= > {
  const hello = enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={5} / >); expect(hello.find(".greeting").text()).toEqual('Hello Daniel!!!!! ');
});

it('throws when the enthusiasm level is 0'.(a)= > {
  expect((a)= > {
    enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={0} / >); }).toThrow(); }); it('throws when the enthusiasm level is negative'.(a)= > {
  expect((a)= > {
    enzyme.shallow(<Hello name='Daniel' enthusiasmLevel={- 1} / >); }).toThrow(); });Copy the code

These tests are very basic, but you should be able to get the gist of things.

Adding Status Management

At this point, if you’re using React to request data once and present it, you might consider doing it yourself. However, if you are developing more interactive applications, you may need to add state management.

State management in general

React is a useful library for creating composable views. However, React doesn’t have any facilities to synchronize data between applications. In the case of the React component, data flows through props to each child element you specify.

Because React itself does not include built-in support for state management, React uses libraries like Redux and MobX.

Redux relies on synchronizing data through a centralized and immutable data store, and updates to that data will trigger a re-rendering of our application. By sending an explicit action message, the state is updated in a constant way, and it must be handled by a function called reducers. Because of its explicit nature, it is often easier to understand how the behavior will affect the state of your program.

MobX relies on a functional reaction mode, where state is passed through observable props. Complete synchronization of any observer’s state is accomplished by simply marking the state as observable. The nice thing is that the library is already written in TypeScript.

Both have different advantages and trade-offs. In general, Redux tends to see more widespread use, so for the purposes of this tutorial, we’ll focus on adding Redux; However, we still encourage you to explore both.

The following sections may have a steep learning curve. We directly recommend familiarizing yourself with Redux through its documentation.

Set the stage for actions

There is no point in adding Redux unless the state of our application changes. We need a source of action that can trigger changes. This could be a timer, or a button-like UI element.

For our purposes, we’ll add two buttons to control the popularity of our Hello component.

Install the story

To add Redux, we first install Redux and React-Redux, along with their types libraries, as dependencies.

npm install -S redux react-redux @types/react-reduxCopy the code

In this example we don’t need to install @types/redux because redux already has its own definition file (.d.ts files).

Define the state of our app

We need to define what state Redux will store. To do this, we can create a file called SRC /types/index.tsx that will contain definitions of the types that may be used throughout the program.

// src/types/index.tsx

export interface StoreState {
    languageName: string;
    enthusiasmLevel: number;
}Copy the code

The intention is that languageName will be the programming language (i.e. TypeScript or JavaScript) for this application and the enthusiasmLevel will change. When we write the first container, we’ll see why we intentionally made our state slightly different from our props.

Add the actions

We’ll start by creating a set of message types that our application can respond to, in SRC/Constants /index.tsx.

// src/constants/index.tsx

export const INCREMENT_ENTHUSIASM = 'INCREMENT_ENTHUSIASM';
export type INCREMENT_ENTHUSIASM = typeof INCREMENT_ENTHUSIASM;


export const DECREMENT_ENTHUSIASM = 'DECREMENT_ENTHUSIASM';
export type DECREMENT_ENTHUSIASM = typeof DECREMENT_ENTHUSIASM;Copy the code

The const/Type pattern allows us to use TypeScript string literal types in an easily accessible and reconfigurable manner.

Next, we will create a set of actions in SRC/Actions /index.tsx, along with the Actions constructor.

import * as constants from '.. /constants'

export interface IncrementEnthusiasm {
    type: constants.INCREMENT_ENTHUSIASM;
}

export interface DecrementEnthusiasm {
    type: constants.DECREMENT_ENTHUSIASM;
}

export type EnthusiasmAction = IncrementEnthusiasm | DecrementEnthusiasm;

export function incrementEnthusiasm() :IncrementEnthusiasm {
    return {
        type: constants.INCREMENT_ENTHUSIASM
    }
}

export function decrementEnthusiasm() :DecrementEnthusiasm {
    return {
        type: constants.DECREMENT_ENTHUSIASM
    }
}Copy the code

We have created two types that describe what is increasing actions and what is reducing actions.

We also created a type (EnthusiasmAction) to describe the situation in which actions can be incremental or decrement. Finally, we made two functions that create actions that we can use instead of writing out large object literals.

There is obvious style code here, so you should always check out libraries like Redux-Actions.

Add the reducer

We are ready to write our first reducer!

Reducers is just a function to copy and modify the state of our application without any side effects. In other words, this is what we call a pure function.

Our reducer will be located below SRC /reducers/index.tsx.

Its function is to ensure that increments increase motivation by 1 point and decrease motivation by 1 point, but at a level of no less than 1. Its function is to ensure that increments increase popularity by 1 and decrements decrease popularity by 1, but the level never goes below 1.

// src/reducers/index.tsx

import { EnthusiasmAction } from '.. /actions';
import { StoreState } from '.. /types/index';
import { INCREMENT_ENTHUSIASM, DECREMENT_ENTHUSIASM } from '.. /constants/index';

export function enthusiasm(state: StoreState, action: EnthusiasmAction) :StoreState {
  switch (action.type) {
    case INCREMENT_ENTHUSIASM:
      return { ...state, enthusiasmLevel: state.enthusiasmLevel + 1 };
    case DECREMENT_ENTHUSIASM:
      return { ...state, enthusiasmLevel: Math.max(1, state.enthusiasmLevel - 1)}; }return state;
}Copy the code

Note that we are using the object expansion operator (… State) this allows us to create a shallow copy of state while replacing the enthusiasmLevel. It is worth noting that the enthusiasmLevel property needs to be put behind otherwise it will be overlaid by properties in the old state.

You may want to write a few tests for your Reducer. Because reducers are pure functions, they can be passed arbitrary data.

For each input, reducers can be tested by examining its newly generated state. Consider studying Jest’s toEqual method to achieve this.

Create a container

When writing Redux, we often write components and containers. Components are usually data agnostic and work primarily at a presentation level. Containers typically wrap components and provide them with whatever data they need to display and modify their state.

You can learn more about this concept in Dan Abramov’s article Presentational and Container Components

First, let us to update the SRC/components/hello.html TSX, so that you can modify state. We will add two optional callback properties named onIncrement and onDecrement to Props:

export interface Props {
  name: string; enthusiasmLevel? :number; onIncrement? :(a)= > void; onDecrement? :(a)= > void;
}Copy the code

We then bind these callbacks to the two new buttons we added to the component.

function Hello({ name, enthusiasmLevel = 1, onIncrement, onDecrement }: Props) { if (enthusiasmLevel <= 0) { throw new Error('You could be a little more enthusiastic. :D'); } return ( <div className="hello"> <div className="greeting"> Hello {name + getExclamationMarks(enthusiasmLevel)} </div>  <div> <button onClick={onDecrement}>-</button> <button onClick={onIncrement}>+</button> </div> </div> ); }Copy the code

In general, for onIncrement and onDecrement, it’s a good idea to have tests that trigger when the corresponding buttons are clicked. Give it a shot to get suspense about your component’s write test.

Try attaching some tests to your component.

Now that our component has been updated, we are ready to wrap it into a container. Let’s create a file called SRC /containers/ hello.tsx and start using the following imports.

import Hello from '.. /components/Hello';
import * as actions from '.. /actions/';
import { StoreState } from '.. /types/index';
import { connect, Dispatch } from 'react-redux';Copy the code

The two real key pieces here are the original Hello component and the connect function from React-Redux. Connect will be able to actually use our original Hello component and turn it into a container using two functions:

  • mapStateToPropsTakes a portion of the data required by the current component from the current store and passes it in.
  • mapDispatchToPropsIt uses the givendispatchThe function triggers actions to our store by creating the callback props.

If we remember, our application state is made up of two properties: languageName and enthusiasmLevel.

On the other hand, our Hello component is expected to have a name and an enthusiasmLevel. MapStateToProps will retrieve the data from the Store and adjust the component’s props as needed. So let’s keep going.

export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
  return {
    enthusiasmLevel,
    name: languageName,
  }
}Copy the code

Note that mapStateToProps creates only two of the four properties expected of a Hello component.

That said, we still want to make onIncrement and onDecrement callbacks. MapDispatchToProps accepts a Dispatcher function as a parameter. The Dispatcher function can trigger updates by passing actions into our store, so we can create a callback function that calls the Dispatcher.

export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
  return {
    onIncrement: (a)= > dispatch(actions.incrementEnthusiasm()),
    onDecrement: (a)= > dispatch(actions.decrementEnthusiasm()),
  }
}Copy the code

Finally, we are ready to call connect.

Connect will first use mapStateToProps and mapDispatchToProps and then return another function that can be used to wrap the component. Our generated container is defined by the following lines:

export default connect(mapStateToProps, mapDispatchToProps)(Hello);Copy the code

When we’re done, our file will look something like this:

// src/containers/Hello.tsx

import Hello from '.. /components/Hello';
import * as actions from '.. /actions/';
import { StoreState } from '.. /types/index';
import { connect, Dispatch } from 'react-redux';

export function mapStateToProps({ enthusiasmLevel, languageName }: StoreState) {
  return {
    enthusiasmLevel,
    name: languageName,
  }
}

export function mapDispatchToProps(dispatch: Dispatch<actions.EnthusiasmAction>) {
  return {
    onIncrement: (a)= > dispatch(actions.incrementEnthusiasm()),
    onDecrement: (a)= > dispatch(actions.decrementEnthusiasm()),
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Hello);Copy the code

Create the store

Let’s go back to the SRC /index.tsx directory. To put it all together, we need to create an initial store and configure it with all the reducer.

import { createStore } from 'redux';
import { enthusiasm } from './reducers/index';
import { StoreState } from './types/index';

const store = createStore<StoreState>(enthusiasm, {
  enthusiasmLevel: 1,
  languageName: 'TypeScript'});Copy the code

The store is… As you might have guessed, the store is the center of our application’s global state.

Next, we’ll replace the./ SRC /components/Hello we’re using with./ SRC /containers/Hello and use the react-redux Provider to connect our props through our container. We import each required part:

import Hello from './containers/Hello';
import { Provider } from 'react-redux';Copy the code

And pass our store to the Provider’s property

ReactDOM.render(
  <Provider store={store}>
    <Hello />
  </Provider>,
  document.getElementById('root') as HTMLElement
);Copy the code

Note that Hello no longer needs props, because we use our connect function to adjust the state of our application for the props of our wrapped Hello component.

Ejecting

If at any point you feel that some create-React-app factor here is making setup difficult, you can always choose to pop up and get the various configuration options you need. For example, if you want to add a Webpack plug-in, you might want to take advantage of the “eject” functionality provided by create-React-app.

Simple run

npm run ejectCopy the code

Go!

Be careful, you may want to commit all your work before running the pop-up. You cannot undo the pop-up command, so the opt-out is permanent unless you can recover from the commit before running the pop-up.

The next step

Create-react-app comes with a lot of good stuff.

Most of this is recorded in the default readme.md generated for our project, so it can be read quickly.

If you want to learn more about Redux, you can check out the official website for documentation, as well as for MobX.

If you want to pop up at some point, you may need to know more about Webpack. You can check out our React & Webpack walkthrough here.

You may need routing at some point. There are several solutions, but the React-Router is the most popular for Redux projects and is often used in conjunction with react-router-Redux.

translator

@author: Riu.Zen

@lastUpdateTime: 2017-10-01

added

2017-10-19 Youdao Cloud notes sharing hang, directly move the source text over, I hope to help you