Earlier this year, I pick up React again to create UIs. Again - because I did not put in any effort to learn the fundamentals of the library in my previous job. I could have easily blame on the fact that we didn’t need such a powerful UI then but really… it’s my laziness that kept me from learning more about React. Shame on me.
Over the past few months, I have learned a lot more about React. The learning curve is steep but just like what I told my interns - read more do more and get the hang of it.
Now I’m going to jot down all the amazing things that I have learned about React (with examples)!
Presentational Components vs Smart Components
I like the idea of separating presentational components and smart components (containers). The idea is dead simple but this React pattern has a huge impact on our codes.
The presentational components are responsible of rendering the components. It does not care about the logic and has no dependencies on the rest of our applications. Most of the time, these components do not need to have states. Hence, they are usually not connect to the store.
The smart components (containers) are responsible of the logic. It fetches data from the server, change states or even do simple arithmetic before providing the data to other components.
Now, presentational components can be highly reusable. For example, we have a bunch of presentational components that are developed to render common charts (e.g. line, dotted). They do nothing except rendering the charts with the given data. I can create two different smart components on separate occasions and reuse the presentation components to create two different line charts.
Yay to reusability!
Storing Consistent States with React Redux
In the past when we used Django as our main templating engine, we have a very big issue.
We were unable to find an easy way to keep our UI states consistent. When we pull some data from our backend asynchronously, we need to ensure that all the containers on our UI reflects the updated data accordingly.
Since the templating engine does not maintain any states, we have to manually update all the containers. This causes a huge consistency problems.
redux
solves this problem for us. Using this library, we can store all the states centrally in a Redux store
. When a state changes, any containers that subscribe to the changes will be notified and re-rendered with the latest state.
Here’s an example: Suppose we have a login UI for our users to log into our web portal given their username and password. The username will be used by other components and we need to store it somewhere so that we don’t have to fetch from the backend again.
First, we create our Redux
store in store.js
.
import { createStore } from 'redux';
import * as reducers from './ducks'; // more on ducks later
const rootReducer = combineReducers(reducers);
const store = createStore(rootReducer);
export default store;
Then, we use React-Redux
to bind Redux
store to our react project. To do so, we setup a Redux store
and use <Provider/>
to make the store available to all our components.
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './states/ducks/store';
import App from './App';
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>
document.getElementById('root')
);
Next, we use connect
to connect our component to the store. In this case, we connect out LoginContainer
to the store so that we can dI will omit the login action creator’s details.
import { connect } from 'react-redux';
import { sessionActions } from './state/ducks/actions/session';
const mapStateToProps = state => {
return {
username: state.sessionState.session.username,
accessToken: state.sessionState.session.accessToken,
}
}
const mapDispatchToProps = dispatch => {
return {
onLogin: (username, password) => {
dispatch(sessionActions.loginRequest(username, password))
}
}
}
class LoginContainer extends React {
...
<omitted>
...
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(LoginContainer);
That’s it! We connected our component to the store. Now the component can receive updates and dispatch new changes to the centralise store. We can also create another container (e.g. ProfileContainer
), which receive changes to username
from the same store when a user logs in the portal.
Let redux-saga
Handles the Side Effects
I was having so much trouble using axios
to handle asynchronous calls. Don’t get me wrong: axios
is great in its work but it doesn’t care about how you handle the side effects.
That’s when we get to know redux-saga
and redux-thunk
. There are differences between the two libraries but it depends on your use cases.
The concept behind redux-saga
is very straightforward: you create a separate thread to handle all the side effects. If you used to develop OS software, you should be very familiar with this. redux-saga
is a redux
middleware. This means that it will work seamlessly with redux
!
Suppose we have a login UI for our users to log in using their username and password.
const mapDispatchToProps = dispatch => {
return {
onLogin: (username, password) => {
dispatch(sessionActions.loginRequest(username, password))
}
}
}
...
When the user clicks a button, the login
component will dispatch an object to the store
. Now, we need to call our API and send the login request to our backend server. We create a saga
that watches for all the sessionActions.loginRequest
actions and triggers an API call to the backend.
import { call, put, takeLatest } from 'redux';
function *login(action){
try{
const loginResponse = yield call(api.login, action.username, action.password);
yield put(sessionActions.loginSuccess(username));
}catch (e){
yield put(sessionActions.loginError(e))
}
}
function *loginSagas(){
yield takeLatest("LOGIN_REQUEST", login);
}
export default loginSagas;
Then we connect our saga to the store
that we have created in store.js
previously:
import createSagaMiddleware from 'redux-saga'
import loginSagas from './sagas';
import { createStore } from 'redux';
import * as reducers from './ducks'; // more on ducks later
const sagaMiddleware = createSagaMiddleware()
const rootReducer = combineReducers(reducers);
sagaMiddleware.run(loginSagas);
const store = createStore(rootReducer, sagaMiddleware);
export default store;
That’s it! The component will dispatch everytime my user logs into the web portal. The API call will be handled asynchronously by redux-saga
. It will yield the side effect (login response in this case) and change the states (login success in this case) accordingly.
Learning The Ducks File Structure
react-redux
is awesome but it poses a new problem as our project grows bigger. When I first started my React project in February this year, my project structure was organised this way:
src
- api
- components
- config
- reducers
- actions
- types
- static
- test
- util
- store.js
- index.js
- App.css
- App.test.js
- registerServiceWorker.js
I used create-react-app
to react my project structure and followed Redux’s todo tutorial to create my first redux structure. I couldn’t comprehand what’s the difference between containers and components so I decided to lump them into components. Hah!
The structure is good as it clearly spells out the functions of each file in individual folder. However, as our project gets bigger, it becomes a painful process to add a new feature. If I add a new advanced search component for my todo app, I will need to modify component, reducers, actions, types and possibly our api.
In another words, it’s not scalable.
This becomes difficult to maintain. When new peep joins us, it’s also difficult for them to pick up the concept. React has a steep learning curve and it’s will be even worse if we can’t even maintain our own project folder well.
My intern was the first victim. He found that it’s difficult to add more features to the project without modifying every single file. So he went beyond what he’s tasked to do and found re-ducks
, which seems to be what we need right now.
We started organising our features into modular duck folders. In each duck folder, it contains the entire logic to handle the feature - reducers, actions, types, api and tests. At the root of each duck folder, we use index.js
to exports our reducers and actions.
Other than organising the redux folders, we have placed our components and containers in a separate folder. We separated them into three other sub-folders. common
stores all the images, icons and css. page
stores the containers and components for individual features. layout
stores the page layout (e.g. top bar and side bar).
At first, we organised all our containers in one folder and components in another.Within one single page, we created multiple containers. As our project gets bigger, it’s back to the same problem so we decided to break them into different features.
The new project structure looks like this:
src
- states
- ducks
- session
- actions.js
- api.js
- index.js
- operations.js
- reducers.js
- session.test.js
- types.js
- <omitted>
- index.js
- store.js
- views
- common
- images
- page
- login
- <omitted>
- layout
- main
- topbar
- sidebar
- footer
Now, this is so much better. If we need to store state for our new feature, we create a new folder in src/states/ducks/
and another one in src/views/page/
. As we finished implementing our states, most of the time we didn’t need make additional changes. Our frontend developers just need to focus in making stuffs in page
work.
Conclusion
I’m really glad that I was given the opportunities to learn about ReactJS in my day job. It’s such a simple but powerful UI library that empowers developers to apply modular approach on the traditional Javascript. I used to hate Javascript because it gets really difficult to manage as the project grows bigger. But now with ReactJS, it has become so much better to scale.
References
react-redux
: https://react-redux.js.orgredux-saga
: https://redux-saga.js.org/re-ducks
: https://github.com/alexnm/re-ducks