Let’s fetch data from the Shoutem Cloud storage to the extension. First, remove the app/assets
folder, we don’t need it anymore. Also remove the getRestaurants()
function from List.js
.
//remove this:
getRestaurants() {
return require('../assets/restaurants.json');
}
Now create a reducer.js
file in the app
folder.
$ cd app
$ touch reducer.js
This file will contain a reducer
defining the initial app state and how the state changes.
Our @shoutem/redux-io package has reducers
and actions
that communicate with the Shoutem CMS. The storage
reducer retrieves data (eg. restaurants) into a dictionary, while collection
stores data ID’s in an array to persist its order.
#file: app/reducer.js
import { storage, collection } from '@shoutem/redux-io';
import { combineReducers } from 'redux';
import { ext } from './const';
// combine reducers into one root reducer
export default combineReducers({
restaurants: storage(ext('Restaurants')),
allRestaurants: collection(ext('Restaurants'), 'all')
});
We’ve used the ext
function to get the full schema name (tom.restaurants.Restaurants
). The root reducer needs to be exported from app/index.js
as reducer
, so your app can find it:
#file: app/index.js
// Reference for app/index.js can be found here:
// http://shoutem.github.io/docs/extensions/reference/extension-exports
import reducer from './reducer';
import * as extension from './extension.js';
export const screens = extension.screens;
export const themes = extension.themes;
export { reducer };
Find more information about extension parts here.
We will fetch restaurants from Shoutem Cloud Storage in the List
screen with the find
action creator. Also, we’ll use three helper functions from our @shoutem/redux-io
package:
#file: app/screens/List.js
import {
find,
isBusy,
shouldRefresh,
getCollection
} from '@shoutem/redux-io';
isBusy
- data is being fetched,shouldRefresh
- should data be (re)fetched,getCollection
- merges storage
dictionary and collection
ID array into an array
of objects.The complete code is for app/screens/List.js
is available below.
Fetch data in the componentDidMount
lifecycle method.
#file: app/screens/List.js
export class List extends PureComponent {
componentDidMount() {
const { find, restaurants } = this.props;
if (shouldRefresh(restaurants)) {
find(ext('Restaurants'), 'all', {
include: 'image',
})
}
}
...
}
Implement rendering with fetched data.
#file: app/screens/List.js
render() {
const { restaurants } = this.props;
return (
<Screen>
<NavigationBar title="RESTAURANTS" />
<ListView
data={restaurants}
loading={isBusy(restaurants)}
renderRow={restaurant => this.renderRow(restaurant)}
/>
</Screen>
);
}
Once fetched, restaurants will go into the app state. Convert them to an array with getCollection
and then connect find
to redux store.
#file: app/screens/List.js
export default connect(
(state) => ({
// get an array of restaurants from allRestaurants collection
restaurants: getCollection(state[ext()].allRestaurants, state)
}),
{ navigateTo, find }
)(List);
This is the final result of List
screen:
#file: app/screens/List.js
import React, { PureComponent } from 'react';
import { TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import { navigateTo, NavigationBar } from 'shoutem.navigation';
import {
find,
isBusy,
shouldRefresh,
getCollection
} from '@shoutem/redux-io';
import {
ImageBackground,
ListView,
Tile,
Title,
Subtitle,
Overlay,
Screen
} from '@shoutem/ui';
import { ext } from '../const';
export class List extends PureComponent {
constructor(props) {
super(props);
// bind renderRow function to get the correct props
this.renderRow = this.renderRow.bind(this);
}
componentDidMount() {
const { find, restaurants } = this.props;
if (shouldRefresh(restaurants)) {
find(ext('Restaurants'), 'all', {
include: 'image',
})
}
}
// defines the UI of each row in the list
renderRow(restaurant) {
const { navigateTo } = this.props;
return (
<TouchableOpacity onPress={() => navigateTo({
screen: ext('Details'),
props: { restaurant }
})}>
<ImageBackground styleName="large-banner" source={{ uri: restaurant.image &&
restaurant.image.url ? restaurant.image.url : undefined }}>
<Tile>
<Title>{restaurant.name}</Title>
<Subtitle>{restaurant.address}</Subtitle>
</Tile>
</ImageBackground>
</TouchableOpacity>
);
}
render() {
const { restaurants } = this.props;
return (
<Screen>
<NavigationBar title="RESTAURANTS" />
<ListView
data={restaurants}
loading={isBusy(restaurants)}
renderRow={restaurant => this.renderRow(restaurant)}
/>
</Screen>
);
}
}
// connect screen to redux store
export default connect(
(state) => ({
// get an array of restaurants from allRestaurants collection
restaurants: getCollection(state[ext()].allRestaurants, state)
}),
{ navigateTo, find }
)(List);
Note
Make sure you remove the
default
fromexport default class List extends Component
and only havedefault
inexport default connect
, because there can only be one default export.
Let’s check how it works:
$ shoutem push
Uploading `Restaurants` extension to Shoutem...
Success!
Works like a charm! You just made your first extension using the Shoutem UI Toolkit and Shoutem Cloud Storage. Great job!