# Components and Contracts ### The React community is very focussed on small components * great for scaling * sharing code * A/B testing
# TypeScript 101 --- ### Some simple types ```javascript const s: string = 'string' const n: number = 100 const arr: number[] = [1,2,3,4] const fn: (name: string) => string = (name: string) => `Hello ${name}`; ``` --- ### TypeScript can infer a lot ```javascript const s = 'string' const n = 100 const arr: number[] = [1,2,3,4] const fn = (name: string) => `Hello ${name}`; ```
# Pure or Presentational Components --- ## Component props * Define the props a component needs * Specify which props are optional * Be specific about the types of props --- ```javascript export interface IPersonProps { // these first props are required name: string; age: number; // the question mark here tells // the compiler that the skills // array is optional skills?: string[]; // We can define a function to be // called when an event happens // within the component. onSelectedSkill?: (skill: string) => any; } ``` --- ## Specifying Props and State ```jsx class Person extends React.Component<IPersonProps, IPersonState> { render() { /* ... */ } } ``` --- ```jsx render() { return ( <section> <h2> My name is {this.props.name} </h2> <h3> I am {this.props.age} years old </h3> <h4>My Skills are:</h4> {this.getSkills()} </section> ); } ``` --- ## Spot the potential runtime error ```jsx getSkills() { if (this.props.skills.length) { const skills = this.props.skills .map((skill: string, i: number) => (<li key={i}>{skill}</li>) ); return <ul>{skills}</ul>; } } ``` --- ## TypeScript will give the error: ```javascript Object is possibly 'undefined'. (property) IPersonProps.skills: string[] | undefined ``` --- ## No Errors ```jsx getSkills() { if (this.props.skills && this.props.skills.length) { const skills = this.props.skills .map((skill: string, i: number) => (<li key={i}>{skill}</li>) ); return <ul>{skills}</ul>; } } ``` --- - Define the props a component needs - Specify which props are optional - Be specific about the types of props
# Connected Components - separate interfaces for props and dispatch for redux connect - joining them together with intersection types --- ### Separating interface definitions for Props and Dispatch ```js export interface IPersonProps { name: string; age: number; skills?: string[]; } // Our event related functions are // moved into this separate dispatch // interface export interface IPersonDispatch { onSelecedSkill?: (skill: string) => any; } ``` --- ### We use the `&` to combine types. ### This is called an Intersection Type ```jsx export class Person extends React.Component< IPersonProps & IPersonDispatch, IPersonState> { } ``` --- ## Tightening generic functions The `connect` function from `react-redux` expects two functions like this ```js type mapStateToProps = (state: IAppState) => object; type mapDispatchToProps = (dispatch: Dispatch) => object; ``` --- ## Tightening generic functions We now have the interfaces we need to be more specific ```js type mapStateToProps = (state: IAppState) => IPersonProps; type mapDispatchToProps = (dispatch: Dispatch) => IPersonDispatch; ``` --- ```js export const PersonWithState = connect( (state: IAppState): IPersonProps => ({ name: state.currentPerson.name, age: state.currentPerson.age, skills: state.currentPerson.skills }), (dispatch): IPersonDispatch => ({ onSelecedSkill: (skill) => { dispatch(selectedSkill(skill)) } }) )(Person); ``` --- - TS tells us what code should be updated when props change - TS makes working with components you have not created easier because the interfaces act like docs
# Actions --- It useful to make all actions have the same basic structure - Generic - Positive - Error - Any or unknown --- ```javascript // We have an action with customisable payload and meta export interface IGenericAction<T, P, M> { // Our type is a const string type: T; // The payload can be anything but will be // specified for a particular action within // the app payload?: P; // The meta property can be used to send // further information along with the action // often used by redux middleware meta?: M; // This is a flag to tell if the payload // is an error error?: boolean; } ``` --- ## Positive ```csharp interface IAction<T, P> extends IGenericAction<T, P, undefined> {} const AUTHENTICATE = 'AUTHENTICATE'; export interface IAuthenticate { username: string; password: string; } type AuthenticateAction = IAction< typeof AUTHENTICATE, IAuthenticate>; ``` --- ## Error ```csharp interface IErrorAction<T, P extends IError> extends IGenericAction<T, P, any> {} type AuthenticateErrorAction = IErrorAction<typeof AUTHENTICATE_ERROR, ResponseError> ``` --- ## Any or unknown ```csharp interface IAnyAction extends IGenericAction<string, any, any> {} ``` --- ```js export interface IAuthenticationState { authenticating: boolean; } // Top level reducer export function authenticateReducer(state: IAuthenticationState, action: IAnyAction) { switch (action.type) { case AUTHENTICATE: return credentials(state, action); default: return state; } } // Function reducer export function credentials(state: IAuthenticationState, action: AuthenticateAction) { const {payload} = action; return { ...state, authenticating: payload.username }; } ``` --- - TS helps us keep structured Actions (Generic, Positive, Error, Any) - TS ensures our action creators output what is expected - TS allows us to check the payload of an action when writing the reducer
# Reducers --- ## Planning our state How might we plan the structure of our resulting state ```csharp interface IAppState { authentication: IAuthenticationState; entities: IEntitiesState; } ``` --- ## Entities We can picture our entities ```csharp interface IEntitiesState { [entityName:string]: IEntityState; // You can define specific entities // if you know of them posts: IEntityState; comments: IEntityState; authors: IEntityState; } ``` --- ## Single Entity ```csharp interface IEntityState<E> { byId: { [id: string]: E }; all: string[]; } ``` --- ## Our Post structure ```csharp interface IPost { id: string; title: string; body: string; } ``` --- ```csharp interface IEntitiesState { // If the entity is unknown then it's // properties could be anything! [entityName:string]: IEntityState<any>; // For our known entity we can define that the // properties will be those defined in the // `IPost` interface posts: IEntityState<IPost>; comments: IEntityState<IComment>; authors: IEntityState<IAuthor>; } ``` --- ## What we are gaining here from all this? ```js let posts = state.entities.posts; ``` - TS knows the specific post properties ... like `title` - TS knows a post has a title `posts.byId[id].title`. - Our `IEntitiesState` will give us a form of documentation - Any reducers which should return data related to the posts entity are now part of this well defined contract --- # Mapping the State to Reducers --- ```csharp interface IAppState { authentication: IAuthState; } ``` Combine reducers is a function which 1. takes that same state (and an action) 2. returns a state of the same shape ```csharp interface ICombineReducers { authentication: ( state: IAuthState, action: IAnyAction ) => IAuthState; } ``` --- # We don't have to maintain two interfaces! # 😱 --- ## Mapped Types ```csharp type ReducersOn<T> = { // For each property P in the object T // map the type to a function where // the first parameter takes state of the same type T[P] // and returns the same type T[P] [P in keyof T]: ( state: T[P], action: IAnyAction ) => T[P]; }; // And then we can declare our root state object const rootState: ReducersOn<IAppState> = { authentication: authenticationReducer, } ``` --- - TS helps us plan our state - TS helps ensure we return the correct structure
# Refactoring --- ## Contrived Reducer ```js export function calculateTotalsReducer( state: IAppState, action: ChangePlayerAction ): IAppState { const { playerTurn, stats } = state; const player = getPlayer(playerTurn); return { ...state, totalsMessage: `Player ${player.name} has ${stats.roomA + stats.roomB} score across all rooms` }; } ``` Notes: `playerTurn` is a number here so `getPlayer` grabs the player details from somewhere else --- We convert our playerTurn from a simple number to an object ![Refactor our playerTurn to include id](07-react-refactoring-player-turn.gif) Notes: We get an error when we call `getPlayer` with the `playerTurn` from the state. --- Error for our reducer because our action is supplying a number ![Refactor our playerTurn to include id](07-react-refactoring-player-turn-state.png) Notes: We will look at each line in the error log next --- The object type in quotes does not match `IAppState`. ```bash index.ts(51,3): error TS2322: Type '{ playerTurn: number | undefined; stats: IStats; totalsMessage: string; }' is not assignable to type 'IAppState'. ``` --- The object does not match `IAppState` because of the `playerTurn` property. ```bash Types of property 'playerTurn' are incompatible. ``` The `playerTurn` property doesn't match because: you're trying to set it to `number | undefined` when `IAppState` wants an `IPlayerTurnState`. ```bash Type 'number | undefined' is not assignable to type 'IPlayerTurnState'. Type 'undefined' is not assignable to type 'IPlayerTurnState'. ``` --- We fix the change player reducer ... ```csharp return { ...state, playerTurn: { id: action.payload || 0 } } ``` --- We fix the calculate totals reducer ![Refactor our playerTurn to include id](07-react-refactoring-player-turn-fix.gif)

Gotchas

  1. External dependencies that are not typed
  2. Mocks and Tests
  3. Typing Higher Order Components like using `connect` and `withRouter`

 

Get Started

  1. davetayls.me has all these in more detail
  2. Join us at Bath JS meetup.com/bathjs/

Come work with Seccl! We're looking for a QA Engineer