React/Redux application with Azure DevOps: Part 7 Use backend from React frontend
Tags: azuredevops, react, redux, unittest
Published: 2020-03-11 06:52:10

---
title: React/Redux application with Azure DevOps: Part 7 Use backend from React frontend
published: true
description: React/Redux application with Azure DevOps
tags: AzureDevOps,React,Redux,UnitTest
---

In the [previous post](https://dev.to/kenakamu/react-redux-application-with-azure-devops-part-6-express-backend-and-database-52ni), I implemented backend service with express.

In this article, I update React side to use the backend.

# Communicate with backend

I see many examples create api folder and place all api related code there. So I follow the pattern. I also use [axios](https://github.com/axios/axios) to talk with backend server.

1\. Make sure you are in root folder and install axios.

```shell
npm install axios @types/axios
```
2\. Create api folder under src folder and add voteAPI.ts

- Re-define IVote and Vote as I cannot access file outside of src with relative path. There maybe better way
- VoteData is for http Request/Response
- I omitted error handling here which I should add (maybe later)

```typescript
/// voteAPI.ts

import axios from 'axios';

export interface IVote {
    id: string;
    votes: number[];
    candidates: string[]
}

export class Vote implements IVote {

    public id: string;
    public votes: number[];
    public candidates: string[];

    constructor(id:string, votes:number[] = [], candidates:string[] = []) {
        this.id = id;
        this.votes = votes;
        this.candidates = candidates;
    }
}

export class VoteData {
    public vote: Vote;
    constructor(vote: Vote) {
        this.vote = vote;
    }
}

class voteAPI {

    private baseUrl = "/api/votes";

    public async getAsync (id: string): Promise<IVote> {
        const url = `${this.baseUrl}/${id}`;
        const { data } = await axios.get<VoteData>(url);
        return data.vote as IVote;
    }

    public async addAsync (vote: IVote): Promise<IVote> {
        const voteData = new VoteData(vote);
        const { data } = await axios.post<VoteData>(this.baseUrl, voteData);
        return data.vote as IVote;
    }

    public async updateAsync(vote: IVote): Promise<IVote> {
        const voteData = new VoteData(vote);
        const { data } = await axios.put<VoteData>(this.baseUrl, voteData);
        return data.vote as IVote;
    }

    public async deleteAsync(id: string): Promise<boolean> {
        const url = `${this.baseUrl}/${id}`;
        const result = await axios.delete(url);
        return result.status === 200
    }
}
export default voteAPI;
```

3\. Add unit test. Mock axios as usual by using jest.

```typescript
/// voteAPI.test.ts

import axios from 'axios';
import VoteApi, {Vote, VoteData } from './voteAPI';

const dummyVote = new Vote('1', [0, 0], ['cat', 'dog']);
const voteApi= new VoteApi();

it('getAsync should return a vote', async () => {
    jest.spyOn(axios, 'get').mockResolvedValue({ data: new VoteData(dummyVote)});
    expect(await voteApi.getAsync('1')).toBe(dummyVote);
});

it('addAsync should return the added vote', async () => {
    jest.spyOn(axios, 'post').mockResolvedValue({ data: new VoteData(dummyVote)});
    expect(await voteApi.addAsync(dummyVote)).toBe(dummyVote);
});

it('updateAsync should return the updated vote', async () => {
    jest.spyOn(axios, 'put').mockResolvedValue({ data: new VoteData(dummyVote)});
    expect(await voteApi.updateAsync(dummyVote)).toBe(dummyVote);
});

it('deleteAsync should return the updated vote', async () => {
    jest.spyOn(axios, 'delete').mockResolvedValue({status:200});
    expect(await voteApi.deleteAsync('1')).toBe(true);
});
```

# Async call from Redux

All the API related call should be from Redux store. I use Redux Thunk to support async call. See [Usage with Redux Thunk](https://redux.js.org/recipes/usage-with-typescript#usage-with-redux-thunk) for more detail.

1\. Update store.ts in redux folder. Simply add ThunkAction and export.

```typescript
/// store.ts

import { configureStore, Action } from '@reduxjs/toolkit';
import { ThunkAction } from 'redux-thunk';
import rootReducer, {RootState} from './reducer/rootReducer';

const store = configureStore({
    reducer: rootReducer
});

export type AppDispatch = typeof store.dispatch;
export type AppThunk = ThunkAction<void, RootState, unknown, Action<string>>
export default store;
```

2\. Update voteSlice.ts.

- Only implement necessary functions, so no add/deleteVote
- Change CountState property hold IVote and message
- To update the status, some functions call reducers internally via dispath

```typescript
/// voteSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AppThunk } from '../store'
import voteAPI, {IVote, Vote} from '../../api/voteAPI';

export interface CountState {
    vote: IVote;
    message: string
}

const client = new voteAPI();
const initialState: CountState = {
    vote: {
        id: '1',
        candidates: ['cat', 'dog'],
        votes: [0, 0]
    },
    message: ''
};

const voteSlice = createSlice({
    name: 'vote',
    initialState: initialState,
    reducers: {
        getVoteSuccess(state: CountState, action: PayloadAction<IVote>) {
            state.vote = action.payload;
            state.message = '';
        },
        updateVoteSuccess(state: CountState, action: PayloadAction<IVote>) {
            state.vote = action.payload;
            state.message = '';
        },
        voteCRUDFailure(state: CountState, action: PayloadAction<string>) {
            state.message = action.payload;
        }
    }
});

export const getVote = (id: string): AppThunk => async dispatch => {
    try {
        const result = await client.getAsync(id);
        dispatch(getVoteSuccess(result));
    } catch (err) {
        dispatch(voteCRUDFailure(err.toString()));
    }
}

export const increment = (vote: IVote, candidate: number): AppThunk => async dispatch => {
    try {
        const newvotes = vote.votes.map((i, index) => index === candidate ? i + 1 : i);
        const newVote = new Vote(vote.id, newvotes, vote.candidates) ;
        const result = await client.updateAsync(newVote);
        dispatch(updateVoteSuccess(result));
    } catch (err) {
        dispatch(voteCRUDFailure(err.toString()));
    }
}

export const decrement = (vote: IVote, candidate: number): AppThunk => async dispatch => {
    try {
        const newvotes = vote.votes.map((i, index) => index === candidate && i > 0 ? i - 1 : i);
        const newVote = new Vote(vote.id, newvotes, vote.candidates) ;
        const result = await client.updateAsync(newVote);
        dispatch(updateVoteSuccess(result));
    } catch (err) {
        dispatch(voteCRUDFailure(err.toString()));
    }
}

export const addCandidate = (vote: IVote, candidate: string): AppThunk => async dispatch => {
    try {
        const newvotes = [ ...vote.votes, 0];
        const newcandidates = [...vote.candidates, candidate];
        const newVote = new Vote(vote.id, newvotes, newcandidates ) ;
        const result = await client.updateAsync(newVote);
        dispatch(updateVoteSuccess(result));
    } catch (err) {
        dispatch(voteCRUDFailure(err.toString()));
    }
}

export const {
    getVoteSuccess,
    updateVoteSuccess,
    voteCRUDFailure
} = voteSlice.actions;

export default voteSlice.reducer;
```

# Async call from React Component

To support async calls in React Component, I can use [Effect Hook](https://reactjs.org/docs/hooks-effect.html), which lets me call async method and update the UI once the operation completed.

One caveat is: 

>By default, effects run after every completed render, but you can choose to fire them only when certain values have changed.

1\. Update voteBoxes.tsx to use useEffect.

- define renderedCandidates for rendering object and change it depending on data
- Trigger useEffect only when props.id is changed.

```typescript
/// voteBoxes.tsx

import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../redux/reducer/rootReducer';
import VoteBox from './voteBox';
import { getVote } from '../redux/reducer/voteSlice';

type Props = {
  id: string
}

const Voteboxes: React.FC<Props> = props => {
  const candidates = useSelector(
    (state: RootState) => state.vote.vote.candidates
  );
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(getVote(props.id));
  }, [dispatch, props.id]);

  let renderedCandidates = candidates !== undefined && candidates.length > 0 ? <div className="voteBoxes">
    {candidates.map((candidate, index) => <VoteBox key={index} index={index} />)}
  </div> : <div>checking</div>;
    
  return renderedCandidates;
}

export default Voteboxes;
```

2\. Update App.tsx to pass properties. I hardcord "1" at the moment.

```typescript
<VoteBoxes id ={'1'} />
```

3\. Update voteBox.tsx as well to accomodate the change.

```typescript
/// voteBox.tsx

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../redux/reducer/rootReducer';
import { increment, decrement } from '../redux/reducer/voteSlice';

interface voteProps {
  index: number
}

const Votebox: React.FC<voteProps> = props => {
  const dispatch = useDispatch();
  const { vote, count, candidate } = useSelector(
    (state: RootState) => {
      return {
        vote: state.vote.vote,
        count: state.vote.vote.votes[props.index],
        candidate: state.vote.vote.candidates[props.index]
      }
    }
  );

  return <div className="voteBox">
    <div>
      {candidate}:{count}
    </div>
    <button onClick={() => dispatch(increment(vote, props.index))}>+</button>
    <button onClick={() => dispatch(decrement(vote, props.index))}>-</button>
  </div>;
}

export default Votebox;
```

4\. Then update candidateBox.tsx.

```typescript
/// candidateBox.tsx

import React, {useState} from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../redux/reducer/rootReducer';
import { addCandidate } from '../redux/reducer/voteSlice';

const CandidateBox: React.FC = () => {
  const { vote } = useSelector(
    (state: RootState) => state.vote
  );

  const [candidate, setCandidate] = useState("");
  const dispatch = useDispatch();
   
  return <div className="candidateBox">
    <input data-testid="input" type="text" value={candidate} onChange={(e) => {
        setCandidate(e.currentTarget.value);
      }} />
    <button onClick={() => {
      dispatch(addCandidate(vote, candidate));
      setCandidate("");
    }
    }>Add candidate</button>
  </div>;
}

export default CandidateBox;
```

Okay, that's it. Thanks to Redux, I didn't need to change much, because most components and data/state operations are already isolated.

# Manual Test

To test the application, I need to run both backend and frontend.

1\. Start backend either by start debugging or simply run npm script in react-backend folder.

```shell
npm run start:dev
```

2\. Run front-end in separate terminal.

```shell
npm start
```

3\. If you want to debug frontend, change the debug profile and hit F5.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/3zk40nccwk7zuzv2frus.jpg)

It's great that I can debug both backend and frontend at the same time :)

If the front end cannot reach to backend, make sure you set proxy in project.json.

# Unit test

The last part is to write unit tests. 

1\. Update App.test.tsx first. It's simple as I just need to pass property to VoteBox component.

```typescript
/// App.test.tsx

import React from 'react';
import ShallowRenderer  from 'react-test-renderer/shallow';
import App from './App';
import VoteBoxes from './components/voteBoxes';
import CandidateBox from './components/candidateBox';
import logo from './logo.svg';

it('render expected component', () => { 
    const renderer = ShallowRenderer.createRenderer();
    renderer.render(<App />);
    const result = renderer.getRenderOutput();
    expect(result.props.children).toEqual(<header className="App-header">
      <VoteBoxes id={"1"}/>
    <CandidateBox />
    <img src={logo} className="App-logo" alt="logo" />
  </header>);
});
```

2\. Update VoteBoxes.test.tsx. I pass different candidates set to test the output.

```typescript
/// VoteBoxes.test.tsx

import React from 'react';
import ShallowRenderer from 'react-test-renderer/shallow';
import VoteBoxes from './voteBoxes';
import VoteBox from './voteBox';
import { useSelector, useDispatch } from 'react-redux';

jest.mock('react-redux');
const useSelectorMock = useSelector as jest.Mock;
const useDispatchMock = useDispatch as jest.Mock;
const dispatchMock = jest.fn();
beforeEach(() => {
  useDispatchMock.mockReturnValue(dispatchMock);
});

it('should render the initial checking', () => {
  useSelectorMock.mockReturnValueOnce([]);
  const renderer = ShallowRenderer.createRenderer();
  renderer.render(<VoteBoxes id={'1'}/>);
  const result = renderer.getRenderOutput();
  //expect(result.props.children.length).toBe(2);
  expect(result.props.children).toEqual("checking")
});


it('should render two VoteBox', () => {
  useSelectorMock.mockReturnValueOnce(['cat','dog']);
  const renderer = ShallowRenderer.createRenderer();
  renderer.render(<VoteBoxes id={'1'}/>);
  const result = renderer.getRenderOutput();
  expect(result.props.children.length).toBe(2);
  expect(result.props.children.toString()).toBe([<VoteBox index={0} />, <VoteBox index={1} />].toString())
});
```

3\. Update candidateBox.test.tsx.

```typescript
/// candidateBox.test.tsx

import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import ShallowRenderer  from 'react-test-renderer/shallow';
import CandidateBox from './candidateBox';
import { useDispatch, useSelector } from 'react-redux';
import { Vote } from '../api/voteAPI';

jest.mock('react-redux');
const useSelectorMock = useSelector as jest.Mock;
const useDispatchMock = useDispatch as jest.Mock;

const dummyFunc = jest.fn();
const dummyVote = new Vote('1', [0, 0], ['cat', 'dog']);

beforeEach(() => {
  useDispatchMock.mockReturnValue(dummyFunc);  
  useSelectorMock.mockReturnValueOnce(dummyVote);
});

it('should render expected element', () => {
  const renderer = ShallowRenderer.createRenderer();
  renderer.render(<CandidateBox />);
  const result = renderer.getRenderOutput();
  expect(result).toMatchSnapshot();
});

it('should call dispatch once when click add candidate', () => {
  const candidate = 'rabbit';
  const { getByText, getByTestId } = render(<CandidateBox />);
  fireEvent.change(getByTestId("input"), { target: { value: candidate } });
  fireEvent.click(getByText(/Add candidate/));
  expect(dummyFunc).toBeCalledTimes(1);
});
```

4\. Finally voteSlice.test.ts. This is a bit more complicated. One important thing is to decide what to test. For example, in my way of writing code, I cannot test the logic for increase/decrease the vote count. If I want to test it, then I need to change the way to test by passing differnt dataset.

- Pass mock dispatch and getState for async call and test with last called function
- Mock async function with jest.spyOn and return the result with Promise.resolve or reject

```typescript
/// voteSlice.test.ts

import vote, {
  getVoteSuccess,
  updateVoteSuccess,
  voteCRUDFailure,
  CountState,
  getVote,
  increment,
  decrement,
  addCandidate
} from './voteSlice'
import { PayloadAction } from '@reduxjs/toolkit';
import voteAPI, {IVote, Vote} from '../../api/voteAPI';

const dummyVote = new Vote('1', [0, 0], ['cat', 'dog']);
const dispatch = jest.fn();
const getState = jest.fn();
const initialState: CountState = {
  vote: {
    id: '1',
    candidates: [],
    votes: []
  },
  message: ''
};

it('should be able to get vote', () => {
  const action: PayloadAction<IVote> = {
    type: getVoteSuccess.type,
    payload:  {
      id: '1',
      candidates: ['cat'],
      votes: [0]
    }
  };
  expect(vote(initialState, action)).toEqual({
    vote: {
      id: '1',
      candidates: ['cat'],
      votes: [0]
    },
    message: ''
  })
});

it('should be able to update vote', () => {
  const action: PayloadAction<IVote> = {
    type: updateVoteSuccess.type,
    payload:  {
      id: '1',
      candidates: ['cat'],
      votes: [0]
    }
  };
  expect(vote(initialState, action)).toEqual({
    vote: {
      id: '1',
      candidates: ['cat'],
      votes: [0]
    },
    message: ''
  })
});

it('should be able to get error', () => {
  const action: PayloadAction<string> = {
    type: voteCRUDFailure.type,
    payload: 'something went wrong'
  };
  expect(vote(initialState, action)).toEqual({
    vote: {
      id: '1',
      candidates: [],
      votes: []
    },
    message: 'something went wrong'
  })
});

it('getVote should dispatch getVoteSuccess on success', async () => {
  jest.spyOn(voteAPI.prototype, 'getAsync').mockReturnValue(
    Promise.resolve(dummyVote));

  await getVote('1')(dispatch, getState, []);
  expect(dispatch).toHaveBeenLastCalledWith(getVoteSuccess(dummyVote));
});

it('getVote should dispatch voteCRUDFailure on failure', async () => {
  jest.spyOn(voteAPI.prototype, 'getAsync').mockReturnValue(
    Promise.reject('error'));
  await getVote('1')(dispatch, getState, []);
  expect(dispatch).toHaveBeenLastCalledWith(voteCRUDFailure('error'));
});

it('increment should dispatch updateVoteSuccess on success', async () => {  
  jest.spyOn(voteAPI.prototype, 'updateAsync').mockReturnValue(
    Promise.resolve(dummyVote));
  await increment(dummyVote, 0)(dispatch, getState, []);  
  expect(dispatch).toHaveBeenLastCalledWith(updateVoteSuccess(dummyVote));
});

it('increment should dispatch voteCRUDFailure on failure', async () => {
  jest.spyOn(voteAPI.prototype, 'updateAsync').mockReturnValue(
    Promise.reject('error'));
  await increment(dummyVote, 0)(dispatch, getState, []);
  expect(dispatch).toHaveBeenLastCalledWith(voteCRUDFailure('error'));
});

it('decrement should dispatch updateVoteSuccess on success', async () => {  
  jest.spyOn(voteAPI.prototype, 'updateAsync').mockReturnValue(
    Promise.resolve(dummyVote));
  await decrement(dummyVote, 0)(dispatch, getState, []);  
  expect(dispatch).toHaveBeenLastCalledWith(updateVoteSuccess(dummyVote));
});

it('decrement should dispatch voteCRUDFailure on failure', async () => {
  jest.spyOn(voteAPI.prototype, 'updateAsync').mockReturnValue(
    Promise.reject('error'));
  await decrement(dummyVote, 0)(dispatch, getState, []);
  expect(dispatch).toHaveBeenLastCalledWith(voteCRUDFailure('error'));
});

it('addCandidate should dispatch updateVoteSuccess on success', async () => {  
  jest.spyOn(voteAPI.prototype, 'updateAsync').mockReturnValue(
    Promise.resolve(dummyVote));
  await addCandidate(dummyVote, 'rabbit')(dispatch, getState, []);  
  expect(dispatch).toHaveBeenLastCalledWith(updateVoteSuccess(dummyVote));
});

it('addCandidate should dispatch voteCRUDFailure on failure', async () => {
  jest.spyOn(voteAPI.prototype, 'updateAsync').mockReturnValue(
    Promise.reject('error'));
  await addCandidate(dummyVote, 'rabbit')(dispatch, getState, []);
  expect(dispatch).toHaveBeenLastCalledWith(voteCRUDFailure('error'));
});
```

After update unit test, confirm everything works as expected.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7q9hmz2x23hrf4l37hdg.jpg)

# Summary

In this article, I consume the backend service which connects to Redis Cache from frontend. I also added unit test with different technics to accommodate the changes.

In the next article, I will look into how to publish the backend and frontend as one application.

[Go to next article](https://dev.to/kenakamu/react-redux-application-with-azure-devops-part-8-integrate-backend-frontend-and-ci-cd-lb3)