Redux: trigger async data fetch on React view event - reactjs

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.

Related

Enable one component do display data from two api fetches

For a react application, I'm using Redux to fetch data from an API. In this application, there exists a component that is displayed twice on the same page. The component is connected to an action and reducer.
Both instances of the component should display different data: one displays someone's job and the other displays someone's phone number. Both of these things are requested in separate API calls, causing the problem that the data of the second call, overwrites the data obtained in the first call in the reducer connected to the component. How would it be possible to make two API calls for such a component that is shown twice, such that both instances of it show the data of either one of these api calls?
I tried the following: make one request and fill the reducer. Then make the other request and merge the results of both in the reducer. However, the problem with this approach is that it is also possible to display only one of the components in the application.
This is the react component:
class Display extends React.Component {
componentWillMount() {
const { fetchpayload } = this.props;
fetchpayload(this.props.parameter);
}
render() {
return (
<h1>{this.props.payload}</h1>
);
}
}
const mapStateToProps = (state) => ({
payload: state.DisplayReducer.payload,
});
const mapDispatchToProps = (dispatch) => bindActionCreators(
{
fetchpayload: payloadAction,
},
dispatch,
);
This is the reducer
const initialState = {
payload: [],
};
export function DisplayReducer(state = initialState, action) {
switch (action.type) {
case 'FETCHED_DISPLAY':
return {
...state,
payload: action.payload,
};
return state;
}
}
The action file makes the request, and dispatches 'FETCHED_DISPLAY'.
just don't override your stuff inside reducer
const InitialState = {
person: {
name: '',
job: ''
}
};
export default (state = InitialState, action) => {
switch(action.type) {
case NAME:
return {
...state.person,
person: {
job: ...state.person.job,
name: action.payload
}
}
case JOB:
return {
...state.person,
person: {
name: ...state.person.name,
job: action.payload
}
}
// do that with whatever property of object person you would want to update/fetch
}
}
}
Hope this helps.

Is it OK to make a REST API request from within a Redux reducer?

I have a React component named ItemList which loads a list of items from an API server and then renders them as a list of Item components.
Each Item has a delete button. When the button is clicked, I want to send a request to the API server to delete the item, then re-render the ItemList.
One way I can think of to make that work is to move both API queries into reducers, and then dispatch actions whenever I want to a) get all items; and b) delete an item. Upon successful completion of those API operations, the reducer will update the store and the ItemList will re-render.
Is that a reasonable approach, or is it a bad idea to put API calls inside of reducers?
Here's a simplified version of the the code I have so far. It doesn't yet use Redux. I want to make sure my approach is sound before implementing Redux, hence this Stack Overflow question.
ItemList.js
class ItemList extends Component {
constructor(props) {
super(props);
this.state = {
items: []
};
this.componentDidMount = this.componentDidMount.bind(this);
}
componentDidMount() {
const url = 'https://api.example.com/api/v1.0/item';
fetch(url, {
method: "get"
})
.then(res => res.json())
.then(response => {
this.setState({items: response.data});
});
}
render() {
<div>
{this.state.items.map((item, index) => (
<Item key={item.id} item={item} />
))}
</div>
}
}
Item.js
class Item extends Component {
deleteClicked() {
/**
* Is it ok to dispatch a "delete item" action
* from here and then make the actual API call
* in a reducer?
*/
}
render() {
<div>
<h2>{item.title}</h2>
<a onClick={this.deleteClicked}>delete item</a>
</div>
}
}
You're almost solved your task. To make you solution perfect use Action creators to make async calls and dispatch actions on completion. Reducer should be pure sync function.
For example ItemList component may use such action creator to extract items
const ExtractItemsAction = () => (dispatch) => {
dispatch ({type: ITEMS_REQUESTED});
const url = 'https://api.example.com/api/v1.0/item';
fetch(url, {
method: "get"
})
.then(res => res.json())
.then(response => {
dispatch({type: ITEMS_RECEIVED, items: response.data});
});
}
And reducer will stay pure
function reducer (state = initalState, action)
{
switch (action.type) {
case ITEMS_REQUESTED:
return { ...state, itemsRequested: true }
case ITEMS_RECEIVED:
return { ...state, itemsRequested: false, items: action.items }
default
return state;
}
}
And don't forget to connect you component to Redux, and use Redux-thunk as middleware when creating store.

How to pass success state back to component without updating store

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.

Clear redux state

I have a component PostsShow which is showing the selected post:
#connect((state) => ({post: state.posts.post}), {fetchPost})
class PostsShow extends Component {
componentDidMount() {
this.props.fetchPost(this.props.match.params.id);
}
render() {
const { post } = this.props;
if (!post) {
return <div></div>;
}
return (
<div>
<Link to='/'>Back</Link>
<h3>{post.title}</h3>
<h6>Categories: {post.categories}</h6>
<p>{post.content}</p>
</div>
);
}
}
The problem is when user first visits the page, fetchPost function populates state section (posts.post) with some data associated with chosen post and when the user chooses another post, he can see old data for 1-2 sec. (before new request is finished).
Actions map:
Click on post #1
Click Back button
Click on post #2
For 1 sec. you can see old (#1) post, until the request is finished and component refreshed with the post (#2) data.
I'm new to whole redux concept, so i'm curious how are you avoiding this kind of behavior?
MY SOLUTION:
I assume that you can create a switch branch, which will modify sate with (posts.post part) with null value and trigger this behavior on componentWillUnmount method. So:
Action:
export function clearPost() {
return {
type: CLEAR_POST,
payload: null
}
}
Reducer:
const INITIAL_STATE = { all: [], post: null };
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
// ... Other cases
case CLEAR_POST:
return { ...state, post: null }
default:
return state;
}
}
Component:
class PostsShow extends Component {
componentDidMount() {
this.props.fetchPost(this.props.match.params.id);
}
componentWillUnmount() {
this.props.clearPost();
}
render() {
// Old render
}
}
Is this a good approach for react with redux?
Your state structure is not ideal. Try keeping your posts like that:
posts: {
byID: {
1: {/*the post* no 1/},
2: {/*the post* no 2/},
// ...
}
allIDs: [2, 1 /*, ...*/],
}
This way you can provide an ordered list of post id's for a list view and show a single post by getting it from the state like: this.posts.byID['thePostID'].
Also read up in the redux docs on how to normalize state.
This will also fix your problem because when your get your post from the store with an id that does not already exist, it will be undefined thus rendering as an empty div. A loading indicator would be the best thing to show.
This is because you are making an async call , which takes time to get new date, that is why when fetch posts gets new data then your component will update and render it.
You can have two approaches:
1) you can create a an async function which will dispatch an action that will have a update the reducer with payload as
status :LOADING,
once the fetch function returns with new data again dispatch an action that
will update the reducer as
status:SUCCESS
and in your component check if status received in the props by store is 'LOADING' or 'SUCCESS', if SUCCESS then show new state, if LOADING keep showing some text like component is Loading:
here is the example
SomeComponent.js
export default class SomeComponent extends Component {
constructor (props) {
super(props)
this.model = {}
}
render () {
const {
status
} = this.props
const loading = status === 'LOADING'
return (
<Layout>
<Row>
<FormField
label='First Name'
id='first-name'
value={details.firstName}
onChange={this.update('firstName’)}
disabled={loading}
mandatory
/>
</Row>
</Layout>
)
}
}
actions.js
const fetchDetails = (someId) => {
return (dispatch) => {
dispatch(dataFetching())
return http.get(dispatch, `https//someCall`, null, {
'Accept': SOME_HEADER
}).then((data) => {
dispatch(dataFetched(data))
}).catch((error) => {
dispatch(someError(`Unable to retrieve the details ${error}`))
})
}
}
const dataFetching = () => ({
type: constants.DATA_FETCHING
})
const dataFetched = (data) => ({
type: constants.DATA_FETCHED,
data
})
Reducer.js
export default function reducer (state = {}, action = {}) {
switch (action.type) {
case constants.DATA_FETCHING:
return Object.assign({}, state, {
data: {},
status: 'LOADING'
})
case constants.DATA_FETCHED:
return Object.assign({}, state, {
data: action.data,
status: 'SUCCESS'
})
default:
return state
}
}
2nd Approach
The one which you did, but for clarity try to use the first one.

Handling Redux Saga result in view from which action call originates

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.

Resources