I'm new to Redux Saga, coming from Redux Thunk. In some situations, I need to know whether an API call fails or succeeds from inside the view from which I called the action. With Redux Thunk, I would do something like the following.
My component and action creator would look like this:
class MyComponent extends Component {
componentDidMount() {
this.props.actions.fetchItems()
.then(result => {
if (result.status === 'OK') {
console.log('Request succeeded, take particular action in this view')
}
else {
console.log('Request failed, take other action in this view')
}
})
}
render() {
return (
<div>My view</div>
)
}
}
const mapStateToProps = (state, ownProps) => {
return {
items: state.items,
}
}
const mapDispatchToProps = dispatch => {
return {
actions: bindActionCreators({
...actions,
}, dispatch),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(MyComponent)
import * as t from './actionTypes'
import {CALL_API} from '../api'
const xhrGetItems = (params) => (dispatch, getState) => {
const action = {
[CALL_API]: {
type: t.XHR_ITEMS_FETCH,
endpoint: `http://www.example.com/myendpoint`,
method: 'get',
}
}
return dispatch(action)
}
My API middleware catches all actions with a CALL_API property, uses an ajax library to make the appropriate call, then returns a fulfilled promise. My reducer is set up to handle each of the possible states of the api call (success, failure, pending). All the while, I can still check the result of the call in my view where it originated.
So my question is, how can this be accomplished with Redux Saga? Right now my saga api middleware is doing everything it should be doing, but when I call fetchItems() in my view, the result is a plain JS object, so I can't check whether or not it succeeded.
It's possible that I'm going about this completely wrong, too. Any suggestions are very much appreciated.
A common pattern with redux and redux-saga is to create 3 Actions for an API call. In your case I would create:
LIST_ITEMS_START
LIST_ITEMS_SUCCEEDED
LIST_ITEMS_FAILED
Your saga would look something like this:
function* watchListItemsStart() {
yield takeLatest(LIST_ITEMS_START, listItemsStartFlow)
}
function* listItemsStartFlow() {
try {
const result = yield call(api.getItems)
if (result.status === 'OK') {
yield put(LIST_ITEMS_SUCCEEDED, items)
} else {
throw new Error(result.error)
}
} catch (error) {
yield put(LIST_ITEMS_FAILED, error)
}
}
The sagas have cleanly abstracted away the API side-effect. The reducer can concentrate on state management:
switch (action.type) {
case LIST_ITEMS_START:
return {
...state,
isLoading: true,
error: null,
items: [],
}
case LIST_ITEMS_SUCCEEDED:
return {
...state,
isLoading: false,
items: action.payload.items,
}
case LIST_ITEMS_FAILED:
return {
...state,
isLoading: false,
error: action.payload.error.getMessage(),
}
}
Now you have everything you need in your store that can be selected and reacted on in the component.
class MyComponent extends Component {
componentDidMount() {
this.props.fetchItems()
}
render() {
const { isLoading, items, error } = this.props
// Here you can react to the different states.
return (
<div>My view</div>
)
}
}
connect(state => ({
isLoading: itemsSelectors.isLoading(state),
items: itemsSelectors.getItems(state),
error: itemsSelectors.getError(state),
}), {
fetchItems: actions.fetchItems
})(MyComponent)
An important point here is, that your component gets very dumb. It just gets props and does not handle the result of the API call (no promise.then here).
I hope this answer will help you.
Related
Im trying to comprehend the art of redux saga, but faced this situation:
I have useEffect hook that works correctly(works one time when changing url params). This hook dispatches action(created by redux-saga-routines) only one time.
const params = useParams().params;
useEffect(() => {
if (urlTriggers.some(item => item === params)) {
dispatch(setItemsCollection({ itemType: params }));
toggleVisibleMode(true);
} else {
toggleVisibleMode(false);
}
}, [params]);
Saga watcher reacts to the dispatched action
export function* setItemsCollectionWatcher() {
yield takeEvery(setItemsCollection.TRIGGER, setItemsCollectionWorker);
}
And then calls saga worker
function* setItemsCollectionWorker(action) {
const { itemType } = action.payload;
try {
yield put(toggleIsFetching({ isFetching: true }));
const itemsCollection = yield call(() => {
return axios.get(`http://localhost:60671/api/items/${itemType}/?page=1&count=2`).then(response => response.data.items);
});
yield put(setItemsCollection.success({ itemsCollection }));
yield put(toggleIsFetching({ isFetching: false }));
} catch (error) {
console.log(error);
} finally {
yield put(setItemsCollection.fulfill());
}
}
This saga listens all saga watchers
export default function* saga() {
yield all([
setBackgroundWatcher(),
setItemsCollectionWatcher(),
])
}
saga running
sagaMiddleware.run(saga);
export const setItemsCollection = createRoutine('showcase/SET_ITEMS_COLLECTION');
export const toggleIsFetching = createRoutine('showcase/TOGGLE_IS_FETCHING');
const showcase = createReducer(
{
itemsCollection: [],
isFetching: false,
},
{
[setItemsCollection.SUCCESS]: (state, action) => {
state.itemsCollection = action.payload.itemsCollection;
},
[toggleIsFetching.TRIGGER]: (state, action) => {
state.isFetching = action.payload.isFetching;
},
}
);
But I have 2 axios requests instead of just one.
Your dispatch from the client is type setItemsCollection which should be fine (though I generally 'USE_HUGE_OBVIOUS_TEXT_LIKE_THIS'). The response from the promise in your saga is the same: setItemsCollection whereas, depending on what you're trying to render, you may want to call your reducer with something entirely different.
At a glance, I'd suggest changing this line to something else (and matching what the reducer is listening for). I wonder if it's causing a crossed wire somewhere.
I have a simple form in react-redux meant to try to add a user to the database, if it is successful, display a success message. However I am not sure of the best approach to do this. I have the following:
onSubmit = e => {
...
const newUser = { user object here }
this.props.registerUser(newUser);
}
in componentWillReceiveProps(nextProps):
if (nextProps.success === true) {
this.setState({ success: nextProps.success });
}
in the render():
Meant to display a success component giving further information. There is also a conditional check to hide the form if success is true
{ this.state.success === true &&
(<SuccessComponent name={this.state.name} />)
}
in mapStateToProps:
const mapStateToProps = state => ({
success: state.success
});
in my action:
.then(res => {
dispatch({
type: REGISTRATION_SUCCESS,
payload: true
});
})
in the reducer:
const initialState = false;
export default function(state = initialState, action) {
switch (action.type) {
case REGISTRATION_SUCCESS:
return action.payload;
default:
return state;
}
}
in combineReducers:
export default combineReducers({
success: successReducer
});
In this, I am basically using the response from the server to dispatch a success prop to the component, update the state, which forces react to render and go through the conditional statement again, hiding the form and displaying the new success block.
However, when I go into redux dev tools, I see that the state from the store is now true, and remains so should users navigate away. Is there a better way to go about this objective? I find that maybe this should be isolated to component state itself, but not sure how to do it since the action and hence the server response is through redux.
Redux is a state machine, not a message bus, so try to make your state values represent the current state of the application, not to send one-time messages. Those can by the return value of the action creator. Success or failure can simply be the existence/lack of an error from the action creator.
If you actually do want to store the user info, you can derive your "was successful" state by virtue of having a registered user, and clear out any existing registered user on component mount.
// actions.js
export const clearRegisteredUser = () => ({
type: SET_REGISTERED_USER,
payload: null,
})
export const register = (userData) => async (dispatch) => {
// async functions implicitly return a promise, but
// you could return it at the end if you use the .then syntax
const registeredUser = await api.registerUser(userData)
dispatch({
type: SET_REGISTERED_USER,
payload: registeredUser,
})
}
// reducer.js
const initialState = { registeredUser: null }
const reducer = (state = initialState, { type, payload }) => {
switch(type) {
case SET_REGISTERED_USER: {
return {
...state,
registeredUser: payload,
}
}
default: {
return state
}
}
}
// TestComponent.js
class ExampleComponent extends Component {
state = {
registrationError: null,
}
componentWillMount() {
this.props.clearRegistered()
}
handleSubmit = async (formData) => {
try {
this.props.register(formData)
} catch(error) {
// doesn't really change application state, only
// temporary form state, so local state is fine
this.setState({ registrationError: error.message })
}
}
render() {
const { registrationError } = this.state
const { registeredUser } = this.props
if (registrationError) {
return <FancyError>{registrationError}</FancyError>
} else if (registeredUser) {
return <SuccessComponent name={this.props.registeredUser.name} />
} else {
return <UserForm onSubmit={this.handleSubmit} />
}
}
}
If you really don't need the user info after you register, you can just perform the api call directly and leave Redux out of it.
class ExampleComponent extends Component {
state = {
registered: false,
registrationError: null,
}
handleSubmit = async (formData) => {
try {
await api.registerUser(formData)
this.setState({ registered: true })
} catch(error) {
this.setState({ registrationError: error.message })
}
}
render() {
const { registered, registrationError } = this.state
if (registrationError) {
return <FancyError>{registrationError}</FancyError>
} else if (registered) {
return <SuccessComponent name={this.props.registeredUser.name} />
} else {
return <UserForm onSubmit={this.handleSubmit} />
}
}
}
Finally, avoid keeping copies of redux state in your component state whenever possible. It can easily lead to sync issues. Here's a good article on avoiding derived state: https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html
First off, I don't think that you should be using your Redux store for saving what is essentially local state.
If it was me I would probably try to make the api call directly from the component and then write to the redux store if it is successful. That way you could avoid having your derived state in the component.
That said, if you want to do it this way I would suggest componentWillUnmount. That would allow you have another Redux call that would turn your registration boolean back to false when you leave the page.
I'm building an App using react-native , following redux concepts , now i got stucked in a problem ,my component is not updating as they should after dispatching an action.
Explaining the plot a bit, I have a React Class "QuestionScreen" and i want to make a call to an API as soon as the page opens but a loader should be rendered while API is doing its work and when its done , questions should appear.so here is my code below
PS: Im new react native and super new to redux concept so a little help with explanation would be nice
QuestionScreen.js
function mapStateToProps(state) {
return {
session: state.questionsReducer.session
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(Actions, dispatch);
}
class QuestionsScreen extends Component {
static navigationOptions = ({navigation}) => ({
title: `${navigation.state.params.title}`,
headerRight: <Button transparent><EntypoIcon name="dots-three-vertical"/></Button>,
headerTintColor: '#0b3484',
});
constructor(props) {
super(props);
this.state = {
params: this.props.navigation.state.params.passProps
};
this.fetchSessionQuestions = this.fetchSessionQuestions.bind(this);
}
fetchSessionQuestions() {
this.props.fetchPracticeQuesions({
URL: BASE_URL + '/api/dashboard/get_chapter_question/',
chapter: this.state.params.chapter_name
});
}
componentDidMount() {
this.fetchSessionQuestions();
}
render() {
const {navigate} = this.props.navigation;
if (this.props.session.isLoading) {
return (
// Loader here
);
}
else {
const questions = this.props.session.questions;
return (
// Some questions logic here
);
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(QuestionsScreen);
questionsReducer.js
import * as types from '../actions/actionTypes';
const initialState = {
session: {
isLoading: true,
currentQuestion: 0,
questions: [],
score: 0,
timeTaken: 0,
attempted: 0
}
};
const newState = JSON.parse(JSON.stringify(initialState));
export default function questionsReducer(state = initialState, action) {
switch (action.type) {
case types.NEXT_QUESTION:
newState.session.currentQuestion = newState.session.currentQuestion + action.payload;
return newState;
case types.FETCH_PRACTICE_QUESTION:
fetch(action.payload.URL, {
method: 'POST',
body: JSON.stringify({
username: 'student',
password: 'pass1234',
chapter: action.payload.chapter
})
}).
then((response) => response.json()).
then((responseJson) => {
newState.session.questions = responseJson;
newState.session.isLoading = false;
});
console.log(newState);
return newState;
default:
return state;
}
}
now the probelem im having is that the props.session in my QuestionScreen remains same, so a loader keeps on spinning, even after dispatching that action, what should i do now ??
and yeah one more thing i checked the states status in console using logger and thunk middleware, the state printed there is as expected , showing me correct value there
You should use middleware as Redux-Thunk to implement async actions. Also you shouldn't use fetch in reducer, you should dispatch 3 events instead on request begin, request success, and request errors. Something like this:
export const callApi = ((fetchUrl) => (dispatch) => {
dispatch(requestBegin());
let body = (method === 'GET') ? undefined : JSON.stringify(data);
return fetch(fetchUrl, {
method,
body,
})
.then(checkStatus)
.then(parseJSON)
.then(function(data) {
dispatch(requestSuccess(data);
}).catch(function(error) {
dispatch(requestError(data));
})
});
Also, you can read about async actions here
I have an action creator that I'm calling in componentWillMount, the return of that action payload is being assigned to state using setState. However, in componentDidMount I cannot access that property as the async call hasn't completed yet. What is the correct way to access this data in compoentDidMount?
//component
class Dashboard extends Component {
componentWillMount() {
this.setState(this.props.getUser());
}
componentDidMount() {
// this.state.user isn't available yet
}
render(){
return(...);
}
}
//action
export function getUser() {
return async function (dispatch) {
const user = await axios.get(`${API_URL}user?token=${token}`);
return dispatch({
type: USER,
payload: user,
});
}
};
}
Axios returns a promise and you have to wait until it resolves. Then dispatch the success action like this,
export function getUser() {
return function (dispatch) {
axios.get(`${API_URL}user?token=${token}`)
.then(user => {
return dispatch(getUserSuccess(user));
}).catch(error => {
throw error;
});
}
};
export function getUserSuccess(user) {
return {type: USER, payload: user};
}
Also note that you need to have mapStateToProps so it brings the user to your component. Then you can access it using this.props.user within your component. It should be like this.
UserPage.propTypes = {
user: PropTypes.object.isRequired
};
function mapStateToProps(state, ownProps) {
return {
user: state.user
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({getUser}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(UserPage);
Finally you may access the user like this.
render() {
const {user} = this.props;
return(
<div>
<div>user.name</div>
</div>
);
}
You need to use componentWillReceiveProps to do that, for example:
componentWillReceiveProps(nextProps) {
if (nextProps.user !== this.state.user) {
this.setState({
user: nextProps.user
});
}
}
now you can use user inside your component.
Here you can find more information.
Completely new to React/Redux, and pretty confused about all the different ways to create components etc. I have read the async docs for Redux and managed to get async data loaded as the page loads, but now I want to use a button to trigger data download.
So far I have a components with this in the render function.
<button onClick={() => dispatch(getEntry('dummy'))}> Get some data< /button>
This is reaching my action creator
export function getEntry(apiroute) {
return {
type: GET_ENTRY,
apiroute
};
}
and redux is passing this to my reducer
export function entry(state = initialState, action) {
switch (action.type) {
case 'GET_ENTRY':
return Object.assign({}, state, {
fetching : true
});
I can see in my logs that the state is being changed by the action.
In my actions file I have this code, which I know works if I wire it into to bootstrapping of my app.
export function fetchEntry() {
return function(dispatch) {
return window.fetch('/google.json')
.then(response => response.json())
.then(json => dispatch(recEntry(json)));
}
}
But where and how should I call fetchEntry in the sequence above following the button click?
The idiomatic way to do async is to use something like redux-thunk or redux-promise. redux-thunk I think is more common though. You used the thunk pattern in your fetchEntry function, but I don't think idiomatically. From the top:
Your button code looks good.
Your getEntry action creator is a bit of a misnomer. You're using it more as initiateGetEntry, right?
Using redux-thunk, you'd combine the two action creators into a thunk action creator:
export function getEntry() {
return function(dispatch) {
dispatch({ type: 'GET_ENTRY_INITIATE' });
fetch('google.json)
.then(function(res) { dispatch({ type: 'GET_ENTRY_SUCCESS, payload: res }) }
.catch(function(res) { dispatch({ type: 'GET_ENTRY_FAIL' });
}
}
The reducer would be similar to what you had:
export function entry(state = initialState, action) {
switch (action.type) {
case 'GET_ENTRY_INITIATE':
return Object.assign({}, state, { fetching : true });
case 'GET_ENTRY_SUCCESS':
return Object.assign({}, state, action.payload, { fetching: false });
case 'GET_ENTRY_FAIL':
return Object.assign({}, state, { fetching: false, error: 'Some error' });
default:
return state;
}
Then, your UI code works fine:
<button onClick={() => dispatch(getEntry('dummy'))}> Get some data< /button>
OK, not sure whether this is the only way, but this worked in my Component
constructor(props) {
super(props);
this.getdata = this.getdata.bind(this);
}
getdata(evt) {
const { dispatch } = this.props;
dispatch(getEntry('getdata'));
fetchEntry()(dispatch);
}
render() {
return (
<div>
<button onClick={this.getdata}> Get some data2</button>
Basically I dispatch a message to the store that an update is starting, and then start the update.
Would welcome confirmation that this is an idiomatic pattern.