Let’s first start with the usual (bad) way of handling loading actions
import { newsActionTypes } from 'src/constants/store/actionTypes';
const initialState = {
cachedNews: {},
lastUpdate: 0,
requestInProgress: false,
refreshing: false,
error: null
};
const newsReducer = (state = initialState, { type, payload }) => {
switch (type) {
case newsActionTypes.UPDATE_NEWS:
case newsActionTypes.DELETE_NEWS:
case newsActionTypes.PUBLISH_NEWS:
case newsActionTypes.FETCH_NEWS:
return {
...state,
refreshing: payload.refreshing,
requestInProgress: true,
error: null
};
case newsActionTypes.FETCH_NEWS_SUCCESS:
case newsActionTypes.PUBLISH_NEWS_SUCCESS:
case newsActionTypes.DELETE_NEWS_SUCCESS:
case newsActionTypes.UPDATE_NEWS_SUCCESS:
return {
...state,
cachedNews: payload.news,
lastUpdate: payload.lastUpdate,
refreshing: false,
requestInProgress: false,
error: null
};
case newsActionTypes.PUBLISH_NEWS_ERROR:
case newsActionTypes.DELETE_NEWS_ERROR:
case newsActionTypes.UPDATE_NEWS_ERROR:
case newsActionTypes.FETCH_NEWS_ERROR:
return {
...state,
refreshing: false,
requestInProgress: false,
error: payload.error
};
default:
return state;
}
};
export default newsReducer;
import { fetchNews } from 'src/store/actions/newsActions';
const NewsComponent= props =>{
const {news}=props;
const {requestInProgress, refreshing, cachedNews}=news;
useEffect(() => {
props.fetchNews();
}, []);
const onRefresh = () => {
const refreshing = true;
props.fetchNews(refreshing);
};
if (requestInProgress && !refreshing) {
return <Loader text="Loading news"}/>;
}
return (
<ScrollView
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
<News cachedNews={cachedNews}/>
</ScrollView>
);
}
const mapStateToProps = state => ({
news: state.news,
});
scrollY: 0,
onRefresh is called, which calls fetchNews, but this time we are refreshing news and the loading indicator is displayed above ScrollView.That’s all good for now, but where is the problem?
To achieve this, we will need to use, instead of boolean, an object that receives the id of the news being deleted.
const initialState = {
...
deleteInProgress: { inProgress:false, newsId:’’}
}
import { newsActionTypes } from 'src/constants/store/actionTypes';
const initialState = {
cachedNews: {},
lastUpdate: 0,
fetchInProgress: false,
publishInProgress: false,
updateInProgress: false,
deleteInProgress: { inProgress:false, newsId:''},
refreshing: false,
error: null
};
const newsReducer = (state = initialState, { type, payload }) => {
switch (type) {
case newsActionTypes.FETCH_NEWS:
return {
...state,
refreshing: payload.refreshing,
fetchInProgress: true,
error: null
};
case newsActionTypes.FETCH_NEWS_SUCCESS:
return {
...state,
cachedNews: payload.news,
lastUpdate: payload.lastUpdate,
refreshing: false,
fetchInProgress: false,
error: null
};
case newsActionTypes.FETCH_NEWS_ERROR:
return {
...state,
refreshing: false,
fetchInProgress: false,
error: payload.error
};
case newsActionTypes.PUBLISH_NEWS:
return {
...state,
publishInProgress: true
};
case newsActionTypes.PUBLISH_NEWS_SUCCESS:
return {
...state,
cachedNews: payload.news,
lastUpdate: payload.lastUpdate,
publishInProgress: false
};
case newsActionTypes.PUBLISH_NEWS_ERROR:
return {
...state,
publishInProgress: false
};
case newsActionTypes.UPDATE_NEWS:
return {
...state,
updateInProgress: true
};
case newsActionTypes.UPDATE_NEWS_SUCCESS:
return {
...state,
cachedNews: payload.news,
lastUpdate: payload.lastUpdate,
updateInProgress: false
};
case newsActionTypes.UPDATE_NEWS_ERROR:
return {
...state,
updateInProgress: false
};
case newsActionTypes.DELETE_NEWS:
return {
...state,
deleteInProgress: {
inProgress: true,
newsId: payload.newsId
}
};
case newsActionTypes.DELETE_NEWS_SUCCESS:
return {
...state,
cachedNews: payload.news,
lastUpdate: payload.lastUpdate,
deleteInProgress: initialState.deleteInProgress
};
case newsActionTypes.DELETE_NEWS_ERROR:
return {
...state,
deleteInProgress: initialState.deleteInProgress
};
default:
return state;
}
};
export default newsReducer;
Let’s separate UI logic from reducers and create uiActions, uiReducer
import { uiActionTypes } from 'src/constants/store/actionTypes';
export const startAction = (name, params) => ({
type: uiActionTypes.START_ACTION,
payload: {
action: {
name,
params
}
}
});
export const stopAction = name => ({
type: uiActionTypes.STOP_ACTION,
payload: { name }
});
export const refreshActionStart = refreshAction => ({
type: uiActionTypes.REFRESH_ACTION_START,
payload: { refreshAction }
});
export const refreshActionStop = refreshAction => ({
type: uiActionTypes.REFRESH_ACTION_STOP,
payload: { refreshAction }
});
import { uiActionTpyes } from 'src/constants/store/actionTypes';
const initialState = {
loader: {
actions: [],
refreshing: []
}
};
const uiReducer = (state = initialState, { type, payload }) => {
const { loader } = state;
const { actions, refreshing } = loader;
switch (type) {
case uiActionTpyes.START_ACTION:
return {
...state,
loader: {
...loader,
actions: [...actions, payload.action]
}
};
case uiActionTpyes.STOP_ACTION:
return {
...state,
loader: {
...loader,
actions: actions.filter(action => action.name !== payload.name)
}
};
case uiActionTpyes.REFRESH_ACTION_START:
return {
...state,
loader: {
...loader,
refreshing: [...refreshing, payload.refreshAction]
}
};
case uiActionTpyes.REFRESH_ACTION_STOP:
return {
...state,
loader: {
...loader,
refreshing: refreshing.filter(refresh => refresh !== payload.refreshAction)
}
};
default:
return state;
}
};
export default uiReducer;
import { call, put, select, takeLeading } from 'redux-saga/effects';
import {
deleteNewsSuccess,
fetchNewsError,
fetchNewsSuccess,
publishNewsSuccess,
updateNewsSuccess
} from 'src/store/actions/newsActions';
import { ApiService } from 'src/services';
import { newsActionTypes } from 'src/constants/store/actionTypes';
import {
startAction,
stopAction,
refreshActionStart,
refreshActionStop
} from 'src/store/actions/uiActions';
export function* fetchNewsSaga({ type, payload }) {
try {
const { refreshing } = payload;
yield put(refreshing ? refreshActionStart(type) : startAction(type));
const response = yield call(ApiService.getNews);
yield put(fetchNewsSuccess(response));
}
} catch (error) {
console.log('fetchNewsSaga error',error);
} finally {
yield put(payload.refreshing ? refreshActionStop(type) : stopAction(type));
}
}
export function* watchFetchNewsSaga() {
yield takeLeading(newsActionTypes.FETCH_NEWS, fetchNewsSaga);
}
export function* publishNewsSaga({ type, payload }) {
try {
yield put(startAction(type));
const { newsData } = payload;
const response = yield call(ApiService.publishNews, newsData);
yield put(publishNewsSuccess(response));
} catch (error) {
console.log('publishNewsSaga error', error);
} finally {
yield put(stopAction(type));
}
}
export function* watchPublishNewsSaga() {
yield takeLeading(newsActionTypes.PUBLISH_NEWS, publishNewsSaga);
}
export function* updateNewsSaga({ type, payload }) {
try {
yield put(startAction(type));
const { newsId, newsData } = payload;
yield call(ApiService.updateNews, newsId, newsData);
const response = yield call(ApiService.getNews);
yield put(updateNewsSuccess(response));
} catch (error) {
console.log('updateNewsSaga error', error);
} finally {
yield put(stopAction(type));
}
}
export function* watchUpdateNewsSaga() {
yield takeLeading(newsActionTypes.UPDATE_NEWS, updateNewsSaga);
}
export function* deleteNewsSaga({ type, payload }) {
try {
const { newsId } = payload;
yield put(startAction(type, { newsId }));
yield call(ApiService.deleteNews, newsId);
const response = yield call(ApiService.getNews);
yield put(deleteNewsSuccess(response));
} catch (error) {
console.log('updateNewsSaga error', error);
} finally {
yield put(stopAction(type));
}
}
export function* watchDeleteNewsSaga() {
yield takeLeading(newsActionTypes.DELETE_NEWS, deleteNewsSaga);
}
import { newsActionTypes } from 'src/constants/store/actionTypes';
const initialState = {
cachedNews: {},
lastUpdate: 0,
error: null
};
const newsReducer = (state = initialState, { type, payload }) => {
switch (type) {
case newsActionTypes.FETCH_NEWS_SUCCESS:
case newsActionTypes.PUBLISH_NEWS_SUCCESS:
case newsActionTypes.DELETE_NEWS_SUCCESS:
case newsActionTypes.UPDATE_NEWS_SUCCESS:
return {
...state,
cachedNews: payload.news,
lastUpdate: payload.lastUpdate,
error: null
};
case newsActionTypes.FETCH_NEWS_ERROR:
return {
...state,
error: payload.error
};
default:
return state;
}
};
export default newsReducer;
export const checkIfLoading = (store, ...actionsToCheck) =>
store.ui.loader.actions.some(action => actionsToCheck.includes(action.name));
export const checkIfRefreshing = (store, actionToCheck) =>
store.ui.loader.refreshing.some(action => action === actionToCheck);
export const getDeletingNewsId = (store, actionToCheck) => {
let newsId = undefined;
for (let i = 0; i < store.ui.loader.actions.length; i++) {
const action = store.ui.loader.actions[i];
if (action.name === actionToCheck) {
newsId = action.params.newsId;
break;
}
}
return newsId;
};
import { deleteNews, fetchNews } from 'src/store/actions/newsActions';
import { checkIfLoading, checkIfRefreshing, getDeletingNewsId, getUserData } from 'src/store/selectors';
const NewsComponent = props => {
const { news, isLoading, refreshing, deletingNewsId } = props;
const { cachedNews, error } = news;
useEffect(() => {
props.fetchNews();
}, []);
const deleteNews = newsId => {
props.deleteNews(newsId);
};
const onRefresh = () => {
const refreshing = true;
props.fetchNews(refreshing);
};
if (isLoading && !refreshing) {
return <Loader text={locales.loadingNews} />;
}
return (
<ScrollView
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
<NewsList
{...{ cachedNews, deleteNews, deletingNewsId }}
/>
</ScrollView>
);
};
const mapStateToProps = state => ({
news: state.news,
deletingNewsId: getDeletingNewsId(state, newsActionTypes.DELETE_NEWS),
isLoading: checkIfLoading(state, newsActionTypes.FETCH_NEWS),
refreshing: checkIfRefreshing(state, newsActionTypes.FETCH_NEWS)
});
const mapDispatchToProps = {
fetchNews,
deleteNews
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(NewsComponent);
What about displaying a loading indicator on a specific news in the list?
function renderDeleteButton(deletingNewsId, deleteNewsAlert, newsId) {
if (deletingNewsId === newsId) {
return <ActivityIndicator />;
}
return (
<CustomButton
text={locales.delete}
onPress={() => deleteNewsAlert(newsId)}
/>
);
}
import { newsActionTypes } from 'src/constants/store/actionTypes';
import { checkIfLoading } from 'src/store/selectors';
function renderButton(isLoading){
return isLoading? <ActivityIndicator/> : <Button/>;
}
const PublishNews = props =>{
const { isLoading } = props;
return (
<View>
<TextInput/>
{renderButton(isLoading)}
</View>
}
const mapStateToProps = state => ({
isLoading: checkIfLoading(state, newsActionTypes.PUBLISH_NEWS, newsActionTypes.UPDATE_NEWS)
});
Summing up
const uiReducer = (state = {}, action) => {
const { type } = action;
const matches = /(.*)_(REQUEST|SUCCESS|ERROR)/.exec(type);
// not a *_REQUEST / *_SUCCESS / *_FAILURE actions, so we ignore them
if (!matches) {
return state;
}
const [requestName, requestPrefix, requestState] = matches;
return {
...state,
// Store whether a request is happening at the moment or not
// e.g. will be true when receiving FETCH_NEWS_REQUEST
// and false when receiving FETCH_NEWS_SUCCESS / FETCH_NEWS_ERROR
[requestPrefix]: requestState === 'REQUEST'
};
};
export default uiReducer;