Skip to content

Commit 0d54e18

Browse files
authored
Merge pull request #9 from arp82/02_Start
02 Create API and expose it in Container to populate the list of members (#3)
2 parents 0548373 + 2692230 commit 0d54e18

19 files changed

+537
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"presets": [
3+
[
4+
"env",
5+
{
6+
"modules": false
7+
}
8+
]
9+
]
10+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "from-react-to-redux-ts",
3+
"version": "1.0.0",
4+
"description": "In this sample we are going to set up the basic plumbing to build our project",
5+
"main": "index.js",
6+
"scripts": {
7+
"start": "webpack-dev-server --inline",
8+
"build": "webpack"
9+
},
10+
"author": "Lemoncode and Front End Master Students",
11+
"license": "MIT",
12+
"devDependencies": {
13+
"@types/react": "^16.0.22",
14+
"@types/react-dom": "^16.0.3",
15+
"awesome-typescript-loader": "^3.3.0",
16+
"babel-core": "^6.26.0",
17+
"babel-preset-env": "^1.6.1",
18+
"css-loader": "^0.28.7",
19+
"extract-text-webpack-plugin": "^3.0.2",
20+
"file-loader": "^1.1.5",
21+
"html-webpack-plugin": "^2.30.1",
22+
"style-loader": "^0.19.0",
23+
"typescript": "^2.6.1",
24+
"url-loader": "^0.6.2",
25+
"webpack": "^3.8.1",
26+
"webpack-dev-server": "^2.9.4"
27+
},
28+
"dependencies": {
29+
"bootstrap": "^3.3.7",
30+
"react": "^16.1.0",
31+
"react-dom": "^16.1.0"
32+
}
33+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# 02 Create API and expose it in Container
2+
3+
In this sample we are going to add an Api to our application and integrate it into our Container component, replacing the previously added hardcoded data.
4+
5+
We will use as start up point _01 Hardcoded list component.
6+
7+
Summary steps:
8+
9+
- Create a data model for the API.
10+
- Implement an API call that fetches data from the web and parses it into the previously defined data model.
11+
- Create an auxiliary mapper module to parse from the api data model to the view model used in the Container component tree.
12+
- Modify the container component to use the API calls and the subsequently returned data.
13+
14+
# Prerequisites
15+
16+
Install [Node.js and npm](https://nodejs.org/en/) if they are not already installed on your computer.
17+
18+
> Verify that you are running at least node v6.x.x and npm 5.x.x by running `node -v` and `npm -v` in a terminal/console window. Older versions may produce errors.
19+
20+
## Steps to build it
21+
22+
- Copy the content of the `01 Hardcoded list component` folder to an empty folder for the sample.
23+
24+
- Install the npm packages described in the `package.json` and verify that it works:
25+
26+
```
27+
npm install
28+
```
29+
30+
- We will start by creating a suitable folder structure for our new API.
31+
```
32+
└── api/
33+
└── model/
34+
├── index.ts
35+
├── member.ts
36+
├── memberApi.ts
37+
└── index.ts
38+
```
39+
40+
- First, we add an `api subfolder` inside `src`. Then, inside `src/api` we create a new subfolder named `src/api/model`
41+
42+
- Let's start by defining the data model used in our API. Our data source will be a list of GitHub members. As we know from the previous case, we want to be able to display the id, name and profile image (represented as an Url) inside our members page. Thus, the data that we really care to retrieve should hold 1 number property for the id, and 2 string properties for the user login name and avatar image Url respectively . Consequently, we will create a `member.ts` file inside `src/api/model` with the content below:
43+
44+
```javascript
45+
export interface MemberEntity {
46+
login: string;
47+
id: number;
48+
avatar_url: string;
49+
}
50+
```
51+
52+
- And since we want to be able to access this interface later from both the API call itself (to properly format the data fetched) and from our auxiliary mapper modulee (to ensure that we can parse from this interface to the one used internally in our view model), we will also define a barrel `index.ts` file for our `src/api/model` folder, as follows:
53+
54+
´´´javascript
55+
export {MemberEntity} from './member';
56+
´´´
57+
58+
- Next we can start working on our `memberApi.ts` file. We will import our data model from `./model` barrel index file. We will also need to define some constants to store the root Url for our data source service, and the specific endpoint we want to call to retrieve the list of members. We can do by adding the following lines.
59+
60+
```javascript
61+
import { MemberEntity } from './model';
62+
63+
const baseRoot = 'https://api.github.com/orgs/lemoncode';
64+
const membersURL = `${baseRoot}/members`
65+
```
66+
67+
- We want to define a get/fetch REST call to retrieve our list of members from the server. In order to do this, we must send an aynchronous call to the server, using a Promise to store said data once it is available in our app. Thus, we define a `fetchMemberList` method that performs the aformentioned 'fetch' operation and parses the corresponding data, as follows:
68+
69+
```javascript
70+
export const fetchMemberList = () : Promise<MemberEntity[]> => {
71+
72+
return fetch(membersURL)
73+
.then(checkStatus)
74+
.then(parseJSON)
75+
.then(resolveMembers)
76+
77+
78+
}
79+
```
80+
81+
- As noted in the code above, we will first fetch the results from our Url endpoint, and then we will first check that the data could be retrieved successfully, parse said data into JSON, and finally resolve said data according to the API data model we have defined.
82+
83+
- Regarding thee `checkStatus` method, we will simply forward the response if we got an OK reply from the Backend. Otherwise, we will throw an error according to the status received. Notice that we do not need to wrap the returned value inside a Promise (for example, using `Promise.resolve()`), as the `then` call already returns a promise resolved with the data returned. Thus, we can chain then properly without incurring any typing errors on behalf of Typescript.
84+
85+
```javascript
86+
const checkStatus = (response : Response) : Response => {
87+
if (response.status >= 200 && response.status < 300) {
88+
return response;
89+
} else {
90+
let error = new Error(response.statusText);
91+
throw error;
92+
}
93+
}
94+
```
95+
96+
- If the members data was retrieved succesfully, we then take the corresponding JSON content.
97+
98+
```javascript
99+
const parseJSON = (response : Response) : any => {
100+
return response.json();
101+
}
102+
103+
```
104+
105+
- And finally, for each object in our data list (i.e. for each member in our members list), we will retrieve the three values we are interested in (using destructuring to make the code more concise), build a new object with these 3 values (using the short syntax for property assignment, i.e. `{id, login, avatar_url} equals {id:id, login:login, avatar_url:avatar_url})`), and finally we 'cast' our object into our api data model, as we do meet the required interface (types match).
106+
107+
```javascript
108+
const resolveMembers = (data : any) : MemberEntity[] => {
109+
const members = data.map(
110+
({id, login, avatar_url,}) => ({ id, login, avatar_url, } as MemberEntity)
111+
);
112+
113+
return members;
114+
}
115+
```
116+
117+
118+
- We have finished our API, now we need to do some changes on our container file and folder to properly expose the API to it.
119+
120+
- First, we will create a new file inside our `src/pages/members` folder called `mapper.ts`. This will be an auxiliary file that parses between our api data model and the view model used in our components. The code we need to add would be the following:
121+
122+
```javascript
123+
import * as apiModel from '../../api/model';
124+
import * as vm from './viewModel';
125+
126+
const mapMemberFromModelToVm = (member: apiModel.MemberEntity) : vm.MemberEntity => (
127+
{
128+
id: member.id,
129+
avatarUrl: member.avatar_url,
130+
name: member.login,
131+
}
132+
)
133+
134+
export const mapMemberListFromModelToVm = (memberList: apiModel.MemberEntity[]) : vm.MemberEntity[] => (
135+
memberList.map(mapMemberFromModelToVm)
136+
)
137+
```
138+
139+
- The method `mapMemberListFromModelToVm` is the one that actually maps the members list retrieved from the Backend into the data model used in our components. Internally, it will call `mapMemberFromModelToVm` to process and parse each member object inside the list. We do not need to use this parsing method outside of our container, so we will not be adding any methods from `mapper.ts` into the `index.ts` file of our container folder.
140+
141+
- The last steps remaining will revolve around changing the code of our `container.tsx` component to use the new API endpoint alongside our mapper's parsing method. First, we will start by adding the new dependencies to our file header.
142+
143+
```diff
144+
import * as React from 'react';
145+
import { MemberListPage } from './page';
146+
import { MemberEntity } from './viewModel';
147+
+ import { fetchMemberList } from '../../api';
148+
+ import { mapMemberListFromModelToVm } from './mapper';
149+
```
150+
151+
- And finally, we will replace the hardcoded block of the `fetchMembers` method to use instead a call to our `fetchMemberList` API endpoint
152+
153+
```diff
154+
export class MemberListContainer extends React.Component<{}, State> {
155+
156+
constructor(props) {
157+
super(props);
158+
this.state = { memberList: [] };
159+
}
160+
161+
fetchMembers = () => {
162+
- setTimeout(() => {
163+
- this.setState({
164+
- memberList: [
165+
- {
166+
- id: 1,
167+
- name: 'John',
168+
- avatarUrl: 'https://avatars1.githubusercontent.com/u/1457912?v=4',
169+
- },
170+
- {
171+
- id: 2,
172+
- name: 'Martin',
173+
- avatarUrl: 'https://avatars2.githubusercontent.com/u/4374977?v=4',
174+
- },
175+
- ]
176+
- });
177+
- }, 500);
178+
+ fetchMemberList().then((memberList) => {
179+
+ this.setState({
180+
+ memberList: mapMemberListFromModelToVm(memberList),
181+
+ });
182+
+ });
183+
}
184+
185+
```
186+
187+
Now if you execute `npm start` and go to `http://localhost:8080/`, you will see the list of members retrieved from our Url.
188+
189+
# About Lemoncode
190+
191+
We are a team of long-term experienced freelance developers, established as a group in 2010.
192+
We specialize in Front End technologies and .NET. [Click here](http://lemoncode.net/services/en/#en-home) to get more info about us.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {fetchMemberList} from './memberApi';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { MemberEntity } from './model';
2+
3+
const baseRoot = 'https://api.github.com/orgs/lemoncode';
4+
const membersURL = `${baseRoot}/members`
5+
6+
export const fetchMemberList = () : Promise<MemberEntity[]> => {
7+
8+
return fetch(membersURL)
9+
.then(checkStatus)
10+
.then(parseJSON)
11+
.then(resolveMembers)
12+
13+
14+
}
15+
16+
// If we return a value that is not a "thennable", it is automatically
17+
// promoted to a promise, so we can keep chaining "then" class without
18+
// wrapping the returned values with Promise.resolve(...)
19+
const checkStatus = (response : Response) : Response => {
20+
if (response.status >= 200 && response.status < 300) {
21+
return response;
22+
} else {
23+
let error = new Error(response.statusText);
24+
throw error;
25+
}
26+
}
27+
28+
const parseJSON = (response : Response) : any => {
29+
return response.json();
30+
}
31+
32+
// This simplified syntax represents the following steps:
33+
// 1- First, we call our function for each entry in the "data" array
34+
// 2- For each of this objects, we then create 3 input varialbes (id, login, avatar_url)
35+
// using destructuring in the signature for the arrow function's arguments
36+
// 3- We create a new object with these 3 variables using the short syntax
37+
// for objects parameters (i.e. {id, login, avatar_url} equals {id:id, login: login,
38+
// avatar_url: avatar_url})
39+
// 4- Finally, we perform a "cast" of this object to MemberEntity TS interface
40+
// (since our properties have the same names and types as the ones in the interface,
41+
// we sort of "implement" it)
42+
const resolveMembers = (data : any) : MemberEntity[] => {
43+
const members = data.map(
44+
({id, login, avatar_url,}) => ({ id, login, avatar_url, } as MemberEntity)
45+
);
46+
47+
return members;
48+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {MemberEntity} from './member';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface MemberEntity {
2+
login: string;
3+
id: number;
4+
avatar_url: string;
5+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title></title>
6+
</head>
7+
<body>
8+
<h1>Sample app</h1>
9+
<div id="root">
10+
</div>
11+
</body>
12+
</html>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as React from 'react';
2+
import * as ReactDOM from 'react-dom';
3+
4+
import { MemberListContainer } from './pages/members';
5+
6+
ReactDOM.render(
7+
<MemberListContainer />,
8+
document.getElementById('root')
9+
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { MemberTable } from './memberTable';
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as React from 'react';
2+
import { MemberEntity } from '../viewModel';
3+
4+
interface Props {
5+
member : MemberEntity;
6+
}
7+
8+
export const MemberRow = (props : Props) => (
9+
<tr>
10+
<td><img src={props.member.avatarUrl} style={{ width: '200px' }} /></td>
11+
<td>{props.member.id}</td>
12+
<td>{props.member.name}</td>
13+
</tr>
14+
);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from 'react';
2+
import { MemberEntity } from '../viewModel';
3+
import { MemberRow } from './memberRow';
4+
5+
interface Props {
6+
memberList : MemberEntity[];
7+
}
8+
9+
export const MemberTable = (props : Props) => (
10+
<table className="table">
11+
<thead>
12+
<tr>
13+
<th>Picture</th>
14+
<th>Id</th>
15+
<th>Name</th>
16+
</tr>
17+
</thead>
18+
<tbody>
19+
{
20+
props.memberList.map(
21+
(member) => <MemberRow
22+
key={member.id}
23+
member={member}
24+
/>
25+
)
26+
}
27+
</tbody>
28+
</table>
29+
);

0 commit comments

Comments
 (0)