|
1 | | -# addons-frontend |
2 | | - |
3 | | -This will outline what is required to add a page to the project. A basic knowledge of |
4 | | -[react](https://facebook.github.io/react/docs/getting-started.html) and |
5 | | -[redux](http://redux.js.org/) is assumed. |
6 | | - |
7 | | -**Note:** This page is very out-of-date and does not reflect our practices anymore. We now use [redux-saga](https://github.com/redux-saga/redux-saga) for API requests. See `amo/components/Categories.js` for a more modern example of a component that makes API/async requests for data. |
8 | | - |
9 | | -## Structure |
10 | | - |
11 | | -A basic app structure will look like this: |
12 | | - |
13 | | -``` |
14 | | -src/ |
15 | | - <app-name>/ |
16 | | - components/ |
17 | | - MyComponent/ |
18 | | - index.js |
19 | | - styles.scss |
20 | | - containers/ |
21 | | - MyContainer/ |
22 | | - index.js |
23 | | - styles.scss |
24 | | - reducers/ |
25 | | - client.js |
26 | | - routes.js |
27 | | - store.js |
28 | | -tests/ |
29 | | - client/ |
30 | | - <app-name>/ |
31 | | - components/ |
32 | | - TestMyComponent.js |
33 | | - containers/ |
34 | | - TestMyContainer.js |
35 | | - reducers/ |
36 | | -``` |
37 | | - |
38 | | -## Components vs Containers |
39 | | - |
40 | | -A component should have no usage of redux in it. It only operates on the data passed into it |
41 | | -through props. A container will use redux to connect data from the store to a component. A |
42 | | -container may or may not wrap a component, whatever makes sense to you. |
43 | | - |
44 | 1 | # How to Add a Page |
45 | 2 |
|
46 | | -We'll make a basic user profile component that hits the API. We'll start by creating a basic |
47 | | -component with data set manually, then we'll hit the API to populate redux, then we'll update the |
48 | | -component to pull its data from the redux store. |
49 | | - |
50 | | -## Creating a Component |
51 | | - |
52 | | -We'll create our component in the search app since it will use the currently logged in user. Our |
53 | | -component is going to show the currently logged in user's email address and username. To start |
54 | | -we'll create a component without any outside data. |
55 | | - |
56 | | -### Basic component |
57 | | - |
58 | | -```jsx |
59 | | -// src/search/containers/UserPage/index.js |
60 | | -import React from 'react'; |
61 | | - |
62 | | -export default class UserPage extends React.Component { |
63 | | - render() { |
64 | | - return ( |
65 | | - <div className="user-page"> |
66 | | - <h1>Hi there!</h1> |
67 | | - <ul> |
68 | | - <li>username: my-username</li> |
69 | | - <li>email: me@example.com</li> |
70 | | - </ul> |
71 | | - </div> |
72 | | - ); |
73 | | - } |
74 | | -} |
75 | | -``` |
76 | | - |
77 | | -### Adding a route |
78 | | - |
79 | | -To render this component we'll tell [react-router](https://github.com/reactjs/react-router) to load |
80 | | -it at the `/user` path. |
81 | | - |
82 | | -```jsx |
83 | | -// src/search/routes.js |
84 | | -// ... omit imports |
85 | | - |
86 | | -export default ( |
87 | | - <Route path="/" component={App}> |
88 | | - <Route component={LoginRequired}> |
89 | | - <Route path="search"> |
90 | | - <IndexRoute component={CurrentSearchPage} /> |
91 | | - <Route path="addons/:slug" component={AddonPage} /> |
92 | | - </Route> |
93 | | - // Add this line to use the `UserPage` component at the `/user` path. |
94 | | - <Route path="user" component={UserPage} /> |
95 | | - </Route> |
96 | | - <Route path="fxa-authenticate" component={HandleLogin} /> |
97 | | - </Route> |
98 | | -); |
99 | | -``` |
100 | | - |
101 | | -### Starting the server |
102 | | - |
103 | | -Now that the component is setup we can run `yarn dev:search` and navigate to |
104 | | -[http://localhost:3000/user](http://localhost:3000/user) to see the page. Since our component is |
105 | | -wrapped in the `LoginRequired` component you will need to be logged in to see the page. |
106 | | - |
107 | | -### Using props |
108 | | - |
109 | | -We want our component's data to be able to change based on the current user. For now we'll update |
110 | | -the component so that it uses props but we won't yet connect it to redux. We will however use |
111 | | -react-redux's `connect()` to set the props for us as if it were pulling the data from redux. |
112 | | - |
113 | | -```jsx |
114 | | -// src/search/containers/UserPage/index.js |
115 | | -import React from 'react'; |
116 | | -import PropTypes from 'prop-types'; |
117 | | -import { connect } from 'react-redux'; |
118 | | -import { compose } from 'redux'; |
119 | | - |
120 | | -class UserPage extends React.Component { |
121 | | - static propTypes = { |
122 | | - email: PropTypes.string.isRequired, |
123 | | - username: PropTypes.string.isRequired, |
124 | | - } |
125 | | - |
126 | | - render() { |
127 | | - const { email, username } = this.props; |
128 | | - return ( |
129 | | - <div className="user-page"> |
130 | | - <h1>Hi there!</h1> |
131 | | - <ul> |
132 | | - <li>username: {username}</li> |
133 | | - <li>email: {email}</li> |
134 | | - </ul> |
135 | | - </div> |
136 | | - ); |
137 | | - } |
138 | | -} |
139 | | - |
140 | | -function mapStateToProps() { |
141 | | - return { |
142 | | - email: 'me@example.com', |
143 | | - username: 'my-username', |
144 | | - }; |
145 | | -} |
146 | | - |
147 | | -export default compose( |
148 | | - connect(mapStateToProps), |
149 | | -)(UserPage); |
150 | | -``` |
151 | | - |
152 | | -Changing the values in `mapStateToProps` will now update the values shown on the page. |
153 | | - |
154 | | -NOTE: You may need to restart your server to see any changes as there is currently a bug that |
155 | | -does not update the server rendered code on the dev server. |
156 | | - |
157 | | -### Making API Requests |
158 | | - |
159 | | -To access the user's data we'll add a new API function and a |
160 | | -[`normalizr`](https://github.com/paularmstrong/normalizr) schema for user objects. Our API function |
161 | | -will be pretty basic since it will use our internal `callApi()` function to handle accessing the |
162 | | -API along with the `normalizr` schema to format the response data. |
163 | | - |
164 | | -```js |
165 | | -// src/core/api/index.js |
166 | | -// ... omit imports |
167 | | - |
168 | | -const addon = new schema.Entity('addons', {}, { idAttribute: 'slug' }); |
169 | | -// Tell normalizr we have "users" and they use the `username` property for |
170 | | -// their primary key. |
171 | | -const user = new schema.Entity('users', {}, { idAttribute: 'username' }); |
172 | | - |
173 | | -// Add this function. |
174 | | -export function fetchProfile({ api }) { |
175 | | - return callApi({ |
176 | | - endpoint: 'accounts/profile', |
177 | | - schema: user, |
178 | | - params: { lang: 'en-US' }, |
179 | | - auth: true, |
180 | | - state: api, |
181 | | - }); |
182 | | -} |
183 | | -``` |
184 | | - |
185 | | -Calling the `fetchProfile()` function will hit the API, now we need to get the data into redux. We |
186 | | -can use the `loadEntities()` action creator to dispatch a generic action for loading data from our |
187 | | -API but we'll need to add a users reducer to store the data. |
188 | | - |
189 | | -```js |
190 | | -// src/core/reducers/users.js |
191 | | -export default function users(state = {}, { payload = {} }) { |
192 | | - if (payload.entities && payload.entities.users) { |
193 | | - return {...state, ...payload.entities.users}; |
194 | | - } |
195 | | - return state; |
196 | | -} |
197 | | -``` |
198 | | - |
199 | | -No we need to tell the app to use the reducer by adding it to our store. |
200 | | - |
201 | | -```js |
202 | | -// src/search/store.js |
203 | | -import users from 'core/reducers/users'; |
204 | | - |
205 | | -export default function createStore(initialState = {}) { |
206 | | - return _createStore( |
207 | | - // Add the `users` reducer here. |
208 | | - combineReducers({addons, api, auth, search, reduxAsyncConnect, users}), |
209 | | - initialState, |
210 | | - middleware(), |
211 | | - ); |
212 | | -} |
213 | | -``` |
214 | | - |
215 | | -We also don't have any record of the user's username. We'll need that to pull the right user. We |
216 | | -can update the `auth` reducer to store it. |
217 | | - |
218 | | -```js |
219 | | -// src/core/reducers/authentication.js |
220 | | -export default function authentication(state = {}, action) { |
221 | | - const { payload, type } = action; |
222 | | - if (type === 'SET_JWT') { |
223 | | - return {token: payload.token}; |
224 | | - } else if (type === 'SET_CURRENT_USER') { |
225 | | - return {...state, username: payload.username}; |
226 | | - } |
227 | | - return state; |
228 | | -} |
229 | | -``` |
230 | | - |
231 | | -We'll also want to add an action creator to set the current user. The action creator just |
232 | | -simplifies interacting with redux to keep the code clean. |
233 | | - |
234 | | -```js |
235 | | -// src/search/actions/index.js |
236 | | - |
237 | | -// Add this at the bottom of the file. |
238 | | -export function setCurrentUser(username) { |
239 | | - return { |
240 | | - type: 'SET_CURRENT_USER', |
241 | | - payload: { |
242 | | - username, |
243 | | - }, |
244 | | - }; |
245 | | -} |
246 | | -``` |
247 | | - |
248 | | -### Combining redux and the API |
249 | | - |
250 | | -Now that we can hit the API and we can store that data in the redux store we need to update our |
251 | | -component to hit the API, update the store, and pull the data from the store. To do this we will |
252 | | -use [redux-connect](https://github.com/makeomatic/redux-connect)'s `asyncConnect()`. |
253 | | - |
254 | | -```jsx |
255 | | -// src/search/containers/AddonPage/index.js |
256 | | -import React from 'react'; |
257 | | -import PropTypes from 'prop-types'; |
258 | | -import { connect } from 'react-redux'; |
259 | | -import { compose } from 'redux'; |
260 | | -import { asyncConnect } from 'redux-connect'; |
261 | | - |
262 | | -import { loadEntities, setCurrentUser } from 'core/actions'; |
263 | | -import { fetchProfile } from 'core/api'; |
264 | | - |
265 | | -class UserPage extends React.Component { |
266 | | - static propTypes = { |
267 | | - email: PropTypes.string.isRequired, |
268 | | - username: PropTypes.string.isRequired, |
269 | | - } |
270 | | - |
271 | | - render() { |
272 | | - const { email, username } = this.props; |
273 | | - return ( |
274 | | - <div className="user-page"> |
275 | | - <h1>Hi there!</h1> |
276 | | - <ul> |
277 | | - <li>username: {username}</li> |
278 | | - <li>email: {email}</li> |
279 | | - </ul> |
280 | | - </div> |
281 | | - ); |
282 | | - } |
283 | | -} |
284 | | - |
285 | | -function getUser({ auth, users }) { |
286 | | - return users[auth.username]; |
287 | | -} |
288 | | - |
289 | | -function mapStateToProps(state) { |
290 | | - return getUser(state); |
291 | | -} |
292 | | - |
293 | | -function loadProfileIfNeeded({ store: { getState, dispatch } }) { |
294 | | - const state = getState(); |
295 | | - const user = getUser(state); |
296 | | - if (!user) { |
297 | | - return fetchProfile({api: state.api}) |
298 | | - .then(({entities, result}) => { |
299 | | - dispatch(loadEntities(entities)); |
300 | | - dispatch(setCurrentUser(result)); |
301 | | - }); |
302 | | - } |
303 | | - return Promise.resolve(); |
304 | | -} |
305 | | - |
306 | | -export default compose( |
307 | | - asyncConnect([{ |
308 | | - deferred: true, |
309 | | - promise: loadProfileIfNeeded, |
310 | | - }]), |
311 | | - connect(mapStateToProps), |
312 | | -)(UserPage); |
313 | | -``` |
314 | | - |
315 | | -### Styling the page |
316 | | - |
317 | | -To style your page you just need to import your SCSS file in your component. All of the CSS will |
318 | | -be transpiled and minified into a single bundle in production so you will still need to namespace |
319 | | -your styles. |
| 3 | +**Note:** this page needs to be expanded. |
320 | 4 |
|
321 | | -```js |
322 | | -// src/search/containers/AddonPage/index.js |
323 | | -// Add this line with the other imports. |
324 | | -import './styles.scss'; |
325 | | -``` |
| 5 | +A basic knowledge of [react](https://facebook.github.io/react/docs/getting-started.html) and [redux](http://redux.js.org/) is assumed. |
326 | 6 |
|
327 | | -```scss |
328 | | -// src/search/containers/AddonPage/styles.scss |
329 | | -.user-page { |
330 | | - h1 { |
331 | | - text-decoration: underline; |
332 | | - } |
333 | | - li { |
334 | | - text-transform: uppercase; |
335 | | - } |
336 | | -} |
337 | | -``` |
| 7 | +We follow the [Ducks proposal](https://github.com/erikras/ducks-modular-redux) to create isolated and self contained modules including reducers, action types and action creators. See `src/core/reducers/autocomplete.js` for an example of a Ducks module. |
| 8 | +We use [redux-saga](https://github.com/redux-saga/redux-saga) for API requests. See `src/amo/components/Categories/index.js` for an example of a component that makes API/async requests for data and `src/core/sagas/autocomplete.js` for an example of a saga. |
| 9 | +Each reducer, saga, component has a corresponding test file. Please refer to it to know how to properly test your code and read [our dedicated page to testing](./testing.md). |
0 commit comments