Building the services
This lesson focusses on building the data-handling services and core Redux management system for the Dinosaur Search App
Building the services and core structure#
We’re going to introduce something a little different in this final project, the concept of services. If you’ve been developing for a little while, the idea of a service layer won’t be anything surprising or new to you, but for the rest of us introducing some data-handling services will give us some separation between different layers of our app.
Ideally, the frontend UI should just concern itself with asking for data, receiving it and then displaying it to the user, allowing them to interact with it. It shouldn’t know (or care) where this data comes from, or how.
That’s the idea behind creating our various [name].service.js
files that we’re going to build in this lesson.
However, for now let’s start with implementing a Redux system using the useReducer
Hook and the Context mechanism, as this whole process should be fresh in your mind from the previous module.
initialState.js#
We’ll begin by opening the initialState.js
file in the /redux
folder. Add the following initialState
object in its entirety:
const initialState = {
auth: {
loading: false,
user: null,
},
dinos: {
loading: false,
favourites: []
}
};
export default initialState;
You might remember that this offers us a good at-a-glance starting point for the sort of structure we want our app’s state to take. We’ll plug this file into our reducers to effect change upon it as each reducer function is called.
At the moment, however, you can see that we have two slices of state: auth
and dinos
. Each has a loading
flag set on it (which we can use to toggle some sort of loading UI in the components) and you can see that the dinos
slice has a favourites
array where we’ll keep track of our favorite dinosaur id values.
At this point, you may be starting to suspect some British involvement in the development of this course! Of course, you would be right (I am a UK developer after all). That's why the
Favourites
component and functions dealing with the favoriting and unfavoriting of dinosaurs contain the British spelling of the word "favourite". Feel free to change these however you wish, just make sure to update all the file and function references from to share the same names or you'll run into errors when you run the project.
Save the file and let’s move on.
authReducer.js#
As you may expect, the authReducer.js
file will handle state updates that relate to the auth
slice that we’ve just seen. Specifically we’re interested in a few state changes:
Trigging a loading status change upon signing a user in.
Updating the
user
object when we’ve successfully finished signing in.Performing a state reset when the user signs out.
Start by creating a set of actions:
// Actions
export const actions = {
FETCH_USER: 'fetching user',
FETCH_USER_SUCCESS: 'finished fetching user',
SIGN_OUT_USER: 'signing out user'
};
The actions
variable is just a plain JavaScript object that houses some hard-coded action strings. Next, it’s time for the physical reducer code:
// Reducer
const authReducer = (state, action) => {
switch (action.type) {
case actions.FETCH_USER: {
return {
state,
loading: true,
};
}
case actions.FETCH_USER_SUCCESS: {
return {
state,
user: action.payload,
loading: false,
};
}
case actions.SIGN_OUT_USER: {
return {
state,
user: null,
loading: false,
};
}
default:
return state;
}
};
export default authReducer;
You can see we have three different switch
cases to handle the three scenarios we outlined earlier. Each one returns a new copy of the state
object, only changing those parts that it needs to.
The only time we’re concerned with using the action
argument passed to the reducer is when the user has successfully signed in and we get a user
object back — you might remember this from the previous lesson where we explored the API.
The complete file#
The file in its entirety now looks like this:
// Actions
export const actions = {
FETCH_USER: 'fetching user',
FETCH_USER_SUCCESS: 'finished fetching user',
SIGN_OUT_USER: 'signing out user'
};
// Reducer
const authReducer = (state, action) => {
switch (action.type) {
case actions.FETCH_USER: {
return {
state,
loading: true,
};
}
case actions.FETCH_USER_SUCCESS: {
return {
state,
user: action.payload,
loading: false,
};
}
case actions.SIGN_OUT_USER: {
return {
state,
user: null,
loading: false,
};
}
default:
return state;
}
};
export default authReducer;
dinoReducer.js#
The dinoReducer.js
file is going to look very familiar to the authReducer
file in its approach. This is something I highlighted in the previous module on Redux, where things might look a little alien and complex to begin with, but once you’ve built a Redux system, extra additions to it start to look familiar.
Let’s define this reducer’s actions:
// Actions
export const actions = {
FETCH_DINOS: 'fetching all dinosaurs',
FETCH_DINOS_SUCCESS: 'finished fetching dinos',
FAVOURITE_DINO: 'favouriting a dinosaur',
UNFAVOURITE_DINO: 'remove favourite dinosaur'
};
This time we have four actions, two for fetching dinosaurs and two to handle the favoriting and unfavoriting of a particular dinosaur.
Let’s add in the reducer body:
// Reducer
const dinoReducer = (state, action) => {
switch (action.type) {
case actions.FETCH_DINOS: {
return {
state,
loading: true,
};
}
case actions.FETCH_DINOS_SUCCESS: {
return {
state,
loading: false,
};
}
case actions.FAVOURITE_DINO: {
return {
state,
favourites: [
state.favourites,
action.payload
],
loading: false,
};
}
case actions.UNFAVOURITE_DINO: {
const updatedDinos = state.favourites.filter(dinoId => dinoId !== action.payload);
return {
state,
favourites: updatedDinos,
loading: false,
};
}
default:
return state;
}
};
export default dinoReducer;
The first two switch
cases essentially just alternate a loading
property from true
to false
and vice versa. I don’t think it hurts to have this loading
state change happen in two separate reducer cases for our learning purposes, but you absolutely could create a TOGGLE_LOADING_STATUS
action and just flip the loading
boolean to its opposite state in one shot.
Further down where we have the favorite-handling parts there is a little more logic, but nothing too complicated. In the first, when the user favorites a dinosaur, we return a copy of state
with the action.payload
value (which will be an id string) tacked onto the end of the favourites
array.
Conversely, when a user unfavorites a dinosaur we need to perform a slightly bigger code dance to filter the current favorites in state
, removing the id value that matches the action.payload
value, and then set the favourites
property in state
to this new array.
The complete file#
The completed reducer file should look like this:
// Actions
export const actions = {
FETCH_DINOS: 'fetching all dinosaurs',
FETCH_DINOS_SUCCESS: 'finished fetching dinos',
FAVOURITE_DINO: 'favouriting a dinosaur',
UNFAVOURITE_DINO: 'remove favourite dinosaur'
};
// Reducer
const dinoReducer = (state, action) => {
switch (action.type) {
case actions.FETCH_DINOS: {
return {
state,
loading: true,
};
}
case actions.FETCH_DINOS_SUCCESS: {
return {
state,
loading: false,
};
}
case actions.FAVOURITE_DINO: {
return {
state,
favourites: [
state.favourites,
action.payload
],
loading: false,
};
}
case actions.UNFAVOURITE_DINO: {
const updatedDinos = state.favourites.filter(dinoId => dinoId !== action.payload);
return {
state,
favourites: updatedDinos,
loading: false,
};
}
default:
return state;
}
};
export default dinoReducer;
reducers.js#
Open up the reducers.js
file and let’s pull everything together to wire up the various parts of our Redux system.
Here’s the code that’s going to power things:
import React, { useReducer, useMemo, createContext } from 'react';
// Data
import initialState from './initialState';
// Reducers
import auth from './authReducer';
import dinos from './dinoReducer';
const combineReducers = reducers => {
return (state = {}, action) => {
const newState = {};
for(let key in reducers) {
newState[key] = reducers[key](state[key], action);
}
return newState;
};
};
const rootReducer = combineReducers({
auth,
dinos
});
export const StoreContext = createContext(null);
export const StoreProvider = ({ children }) => {
const [state, dispatch] = useReducer(rootReducer, initialState);
const store = useMemo(() => [state, dispatch], [state]);
return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
};
// Helper
export const createAction = (type, payload) => ({
type, payload
});
This might look like a lot to drop in all in one go unexplained, but we’re not going to dwell on the details here. The keen-eyed among you will notice that this is almost identical to the reducers.js
file from the last lesson in the previous module on Redux. The only difference is that this time we have two reducers to import, namely auth
and dinos
.
We import those reducers and pass them to the combineReducers
function which will smush them together and handle different updates to different slices of state
for us, whilst we just worry about calling a single dispatch
function to do the job.
If you would like to revisit this file and how it works, please head over to the previous module on Redux for a full breakdown and step-by-step walkthrough of what each part does.
Save this file. Now it’s time for some services.
Services#
As we explained at the beginning of the lesson, building a service layer gives us a greater degree of separation between the different parts of the app. By building out some service handlers we can remove the responsibility of talking to the API from the UI components. They don’t need to concern themselves with talking to the Redux store either.
With service handlers in place they just need to ask a particular service for data, receive it and then process it accordingly. What’s also nice about this approach is that later down the line we could change the service handler to use a JSON file instead of an API, or talk to a database directly, and the components calling this service would never need to know about it.
Each service will manage a particular aspect of data interaction, such as authentication and dealing with the API. Each one will talk to the API, supply and request information as appropriate and call out to the Redux store to dispatch any updates to our app’s global state system.
api.service.js#
Let’s start with the api.service.js
file. This will be a service to service other services (try saying that three times fast!). Essentially the API service will be the direct link to our API. It’ll handle any and all API calls, formatting incoming data and returning any response from the API to the caller.
Open it up and let’s fill it out, starting with a bare scaffold:
import axios from 'axios';
const baseUrl = '/api';
const getUrl = url => `${baseUrl}${url}`;
class ApiService {
get(url) {
}
post(url, data) {
}
};
export default new ApiService();
We’re pulling in axios
to help with our physical API calls. Next we have a baseUrl
variable which is a simple string, /api
. At the moment all of our API calls start with this string path, but we don’t want to have to litter each function we create with it, and if it changes that means more work. We can stash it in a variable here for reference later.
Next, the getUrl
function is a simple, one-line arrow function that uses JavaScript's string templating syntax to return us a resulting API endpoint that begins with /api
and appends whatever URL was passed to it as a parameter.
Finally we have a barebones JavaScript class ApiService
that contains two methods, get()
and post()
which will handle GET
and POST
calls to the API respectively.
Adding the axios calls#
Let’s flesh out the two class methods:
This page is a preview of Beginner's Guide to Real World React