04. Fetching Data for Server-Side-Render
SEO matters. We need to think about our page to be as meaningful as possible. Score to SSR and Treats! Treats supports SSR, but did you realize that all data still not server-side rendered? Well to put it simple it's because we didn't configure the GraphQL SSR. On the different side, when you are not using GraphQL, there's no one would give the data to Treats server. This tutorial will show you how to fetch data for SSR in Treats.
Let's add some case for this tutorial. How about adding country of origin buttons in our profile page? We will get countries data from https://restcountries.eu/rest/v2/all
. When user click on a country button, it will update user's nationality and store it to Redux. Now let's get going to our profile page.
Fetch Data from API
The key concept of fetching data via SSR is using static async methods to dispatch action. Read more about SSR concept here.
- Add
getInitialState
in ourprofile.js
/* src/page/profile-page/profile.js */
class Profile extends Component {
static async getInitialState({ router, serverContext }) {
const { req, reduxStore } = serverContext;
return reduxStore.dispatch(profileActions.getInitialState());
}
...
}
- Next, we need to implement
getInitialState
in oursrc/redux/profile-page/action.js
, along withgetCountries
,getCountriesLoading
,getCountriesSuccess
, andgetCountriesError
. You will need to install axios to help us with the request.
~$ npm install axios
/* src/redux/profile-page/action.js */
/* Other import */
import axios from "axios"
/* Other methods */
const getCountriesLoading = () => ({
type: types["GET_COUNTRIES_LOADING"]
});
const getCountriesSuccess = response => ({
type: types["GET_COUNTRIES_SUCCESS"],
response
});
const getCountriesError = () => ({
type: types["GET_COUNTRIES_ERROR"]
});
const getCountries = () => dispatch => {
dispatch(getCountriesLoading());
return axios.get("https://restcountries.eu/rest/v2/all")
.then(response => {
const parsedResponse = response.data.map(country => ({
code: country.alpha2Code,
name: country.name
}))
dispatch(getCountriesSuccess(parsedResponse));
}).catch(err => {
dispatch(getCountriesError());
console.error(err);
});
};
const getInitialState = () => dispatch => {
return Promise.all([dispatch(getCountries())])
}
export default {
/* Other export */
getCountries,
getInitialState
}
As the name implies, the getCountries
is used to get list of countries from https://restcountries.eu/rest/v2/all
. The getCountries(Loading|Success|Error)
are action generators for updating data fetch status [success, error, loading] in Redux.
- Adding action means we need to define the types, reducer, and initial-state.
/* src/redux/profile-page/type.js */
const types = {
/* Other types */
"GET_COUNTRIES_LOADING": "GET_COUNTRIES_LOADING",
"GET_COUNTRIES_SUCCESS": "GET_COUNTRIES_SUCCESS",
"GET_COUNTRIES_ERROR": "GET_COUNTRIES_ERROR"
};
export default types;
/* src/redux/profile-page/initial-state.js */
const initialState = {
name: "User",
countries: {
status: "loading",
data: []
}
};
export default initialState;
/* src/redux/profile-page/reducer.js */
...
const profileActionHandlers = {
/* Other handlers */
[types.GET_COUNTRIES_LOADING]: state => ({
...state,
countries: {
status: "loading",
data: []
}
}),
[types.GET_COUNTRIES_SUCCESS]: (state, action) => ({
...state,
countries: {
status: "success",
data: action.response
}
}),
[types.GET_COUNTRIES_ERROR]: state => ({
...state,
countries: {
status: "error",
data: []
}
})
};
...
Connecting Component with New Redux State
Now that we already implement the getInitialState
, we need to add interaction to our profile page. As you can see from above implementations, we will store our data into our Redux state (countries). Therefore, we need to get the data via props.
- Now, get back to our
profile.js
and render the data. Also update themapStateToProps
andmapDispatchToProps
.
class Profile extends Component {
/* Other methods */
render() {
const { intl, name, nation, countries: { status, data } } = this.props,
{ name: temporaryName } = this.state;
return(
<div>
<h3>Hello, {name}</h3>
<form onSubmit={this.handleFormSubmit}>
<input value={temporaryName} onChange={this.handleInputChange} />
<button>{intl.formatMessage({id: "submit"})}</button>
</form>
{status === "success" && data.map(country => (
<button
key={country.code}
className={nation === country.name ? style.button_selected : undefined}
onClick={() => this.handleCountryChange(country.name)}
>
{country.name}
</button>
))}
<br />
<Link href="/" isPush>Back to Todo</Link>
</div>
)
}
}
const mapStateToProps = state => ({
name: state.profile.name,
nation: state.profile.nation,
countries: state.profile.countries
});
const mapDispatchToProps = dispatch => ({
onFormSubmit: name => {
dispatch(profileActions.updateName(name))
},
onNationUpdate: country => {
dispatch(profileActions.updateNation(country))
},
onMount: () => {
dispatch(profileActions.getCountries())
}
});
- We also need to update the state, when user click on a country button. Simply add the implement this
handleCountryChange
method inside your Profile class.
class Profile extends Component {
/* Other methods */
handleCountryChange = country => {
const { onNationUpdate } = this.props;
if (onNationUpdate) {
onNationUpdate(country);
}
}
...
}
- Oops! Almost forgot to add the
updateNation
in ouraction.js
. Let's add it onaction.js
,type.js
,initial-state.js
, andreducer.js
/* src/redux/profile-page/action.js */
/* Other actions */
const updateNation = country => ({
type: types["UPDATE_NATION"],
nation: country
});
export default {
/* Other actions */
updateNation,
}
/* src/redux/profile-page/type.js */
const types = {
/* Other types */
"UPDATE_NATION": "UPDATE_NATION",
};
export default types;
/* src/redux/profile-page/initial-state.js */
const initialState = {
name: "User",
nation: "",
countries: {
status: "loading",
data: []
}
};
export default initialState;
/* src/redux/profile-page/reducer.js */
...
const profileActionHandlers = {
/* Other reducers */
[types.UPDATE_NATION]: (state, action) => ({
...state,
nation: action.nation
}),
};
...
- It's confusing to distinguish between selected button and unselected one. So let's add css in
profile.css
for selected button.
.button_selected {
background-color: #0f0
}
- Right now, you should see your profile page is already server-side-rendered. To prove this, you can right-click on your page and click on "View Page Source". You should see that the countries data is rendered along with the components.
Showing the Country Value on Todo Page
Now that we have set "nation" value in Redux state. We can use it in our Todo page.
- Change the
<FormattedMessage>
second value to "nation". Also updatemapStateToProps
so it could pass the state into component props.
const Todo = ({ intl, name, nation }) => (
<div>
<Link href="/profile" isPush>Change Name</Link>
<br />
<FormattedMessage
id="todo_title"
values={{
name,
nation
}}
/>
...
</div>
)
const mapStateToProps = state => ({
name: state.profile.name,
nation: state.profile.nation
})
- Change our locale JSON value, so it could show some related message
/* src/_locale/id.json */
{
"todo_title": "Rencana Saya. Nama saya: {name}. Saya dari: {nation}",
/* Other locale */
}
/* src/_locale/en.json */
{
"todo_title": "My Todo List. My name is: {name}. I am from: {nation}",
/* Other locale */
}
Great job! We just add some functionality to our todo apps. Also, we manage to learn some of Treats concept by practice. Keep exploring Treats features by reading the main-concepts and API references section. If you happen to find issues, don't hesitate to write the issues on our Github.