redux-saga, call function on network complete? - reactjs

I'd like to call a component's function when network fetch completes.
function callRestApi({config, schema}) {
return axios(config).then((response) => {
if (schema) {
var data = normalize_json(response.data, schema)
response.entities = data.entities
}
return response
})
}
function* fetchEventList(action) {
try {
const response = yield call(callRestApi, action.payload);
// here I want to call a component's method if possible
yield put({type: action.response.action_type_success, response});
} catch (e) {
}
}
I can think of two ways to do this, and wonder if one is prefered over another or if there's a better way?
method1:
I include the component in the action payload so that I can call the method
method2:
on action.response.action_type_success, change redux state.
Then, component's componentWillReceiveProps compare if the state variable changed and calls the method

The second. You are using redux-saga to handle side effects, so keep it that way. You could add a callback to the action as method1 but I wouldn't mix concepts.
If you update the store on success, it will re-render the component and as you said you could check the newly updated prop in componentWillReceiveProps and trigger the function, however, check nextProps instead of this.props (but I bet you already know that).
This way everything flows one way, no callback hell :) + you can easily test the component just by passing a prop.
Although it's not a bad pattern per se, passing callbacks would be bi-directional flow, which breaks the first rule of flux: Unidirectional flow.

Related

Changing state directly after action has been dispatched

I'm working on a simple quiz application and want to dispatch an action (called FreeResponseSubmit) to store the user's input (called searchField) into the answers object and then reset the form field. I've tried to chain promises, and while the answers object is updated with the user's input the second "then" doesn't work as planned (I'm assuming the response from the previous promise isn't usable), and the searchField value is never reset.
Are promises even the right way to go on this or is async/await a better route? I've been racking my brain for some time trying to figure this out (still new to these frameworks) so any help is greatly appreciated.
free-response.component:
handleClick(){
const searchFieldPromise= new Promise((resolve, reject)=>{
resolve(this.state.searchField);
});
searchFieldPromise.then((searchField)=>this.props.freeResponseSubmit(searchField))
.then((value)=>this.setState({searchField:""}));
}
The logic within the handleClick method can actually be simplified into this:
const { searchField } = this.state;
const { freeResponseSubmit } = this.props;
freeResponseSubmit(searchField);
this.setState({
searchField:''
});
This is because the action for freeResponseSubmit would have already taken in the value from your component's state, thus carrying out any subsequent operations within redux. Therefore, you can just clear the component's state.

set state in a callback of an async function

I am new to React, so bear with me please. I have a component that calls another component that takes a property. This property will get it's value on a callback of a function, something like this:
render(){
myFunc((p) => {
if(!_.isEqual(p, this.state.myProp))
this.setState({myProp: p})
});
return <MyComponent myProp={this.state.myProp}/>
}
myFunc will or will not make an API request and depending on that will call the callback sooner or later. This seems to work fine when API request is made and the callback takes longer to return. However, when the request is not needed and callback returns instantaneously (or almost) I am getting a Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
What am I doing wrong and what is the right way to approach this? Where would be the right place to put this code? Basically what I need is to re-render MyComponenent if this.state.myProp changes
You shouldn't be calling setState inside the render method, you might end up having an infinite loop.
The call to myFunc should be somewhere else (depending on the business logic you have). When the function finishes, it will update the state and then trigger a re-render so MyComponent will get the latest value.
UPDATE
I don't know which conditions will require calling myFunc again, but you can do:
state = {
myProp: null // or some other value that MyComponent can handle as a null state
}
componentDidMount () {
myFunc((p) => {
if(!_.isEqual(p, this.state.myProp)) // This is needed only if you get null back from the callback and you don't want to perform an unnecesary state update
this.setState({myProp: p})
}
}
render(){
const { myProp } = this.state
// You can also do if (!myProp) return null
return <MyComponent myProp={myProp}/>
}

injecting a url parameter into an an API call in react native

I'm passing some params from a page to the other to make an API call. What i've notice is when i hard code the value i'm suppose to get into the http call, i get the desired results but when i inject the value from the params into the call, if fails to work. so i thought the params wasn't able to pass till i did an alert in the render function and what i realized was the alert prompts twice, the first one is empty and the second prompt brings the value from the previous page, so then meaning in my componentDidMount, the call
state = {
modalVisible: false,
posts : [],
itemID: ''
}
componentDidMount = () => {
const { navigation } = this.props;
this.setState({itemID : navigation.getParam('itemID')})
api.get('/items/'+`${this.state.itemID}`+'/test_items_details/'+`${userID}`+'/posts').then((response) => {
let data = response.data.data
this.setState({posts: data})
console.log(JSON.stringify(this.state.posts))
})
}
As per the docs, it states
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
Your code snippet is trying to access the state variable before it has in fact been updated.
You can do two things here:
Simply use navigation.getParam('itemID') so now your URL becomes /items/${navigation.getParam('itemID')}/test_item_details/${userID}/posts.
Use the setState callback, the snippet is added below.
Snippet:
this.setState({ itemID: navigation.getParam('itemID') }, () => {
// this.state.itemID is now set, you can make your API call.
});
If your only intention to use this.state.itemID is for the URL, I would opt for the first option.

React, Redux: how to avoid loading data into the store twice

The Set Up
I have a React/Redux application that loads a list of cats from an API.
The data gets loaded into a component like so:
// thunk, etc omitted for clarity.
componentDidMount() {
if(!this.props.loaded){
this.props.actions.loadRooms();
}
}
Which draws its props from here:
function mapStateToProps(state, ownProps) {
return {
cats: state.cats.items,
loaded: state.cats.loaded
}
}
Assume the following:
1) cats will be needed in a different, entirely separate component, one that is not a child of the current component.
2) I have no way of knowing which of the cats requiring components will be mounted first.
The Actual Question
Is the if(!this.props.loaded) useful? Put another way, does it save me a theoretical call to the API when that other route mounts if both check for existing store data first?
If the check is useful, is there a better way to do it?
Yes, I would have your redux actions look something like: GET_CATS, GET_CATS_SUCCESS, and GET_CATS_ERROR.
GET_CATS would set the loading state in the redux store to true, that way you can interrogate it in the respective componentDidMount() functions and only make the call to the api when loading is false. I think this is a fairly common way of doing it.
It all depends on how you handle your async data fetching in redux ,if both siblings components are listening to the portion of the state that represents cats you can do:
// Component A and Component B might have something like this
// they both subscribe to the same portion of the state so, if
// data is already available then you don't need to do fetch it again.
...
componentDidMount() {
if (this.props.cats.length === 0) {
this.props.actions.loadRooms();
}
}
...
If you are using redux-thunk then you might control this at the action level:
function loadRooms() {
return (dispatch, getState) => {
if (getState().cats.length === 0) {
dispatch(loadRoomsPending());
fetchMyData(...args)
.then((res) => dispatch(loadRoomsSuccess(res))
.catch((err) => dispatch(loadRoomsError(err));
}
}
}
// Component A and Component B
...
componentDidMount() {
this.props.actions.loadRooms();
}
...
Again here you have access to the current state with getState() so it's pretty common to check if the data is already available. Now this approach comes with some boilerplate and it might get tedious in the long run (it requires for you to write another three functions loadRoomsPending, loadRoomsSuccess, loadRoomsError). This way your components don't have to manually check for it. Or if you like it more explicit or cleaner you can give a middleware I implemented a try, I was kind of frustrated by all this boilerplate so using redux-slim-async you can do this:
function loadRooms() {
return {
types: [
actionTypes.LOAD_ROOMS_PENDING,
actionTypes.LOAD_ROOMS_SUCCESS,
actionTypes.LOAD_ROOMS_ERROR,
],
callAPI: fetch(...args).then(res => res.json()),
shouldCallAPI: (state) => state.cats.length === 0,
};
}
This handles everything for you with FSA compliant actions and it's very clear what is going on. Heck if you set it up properly you can make it even better:
function loadRooms() {
return {
typePrefix: actionTypes.LOAD_ROOMS,
callAPI: fetch(...args).then(res => res.json()),
shouldCallAPI: (state) => state.cats.length === 0,
};
}
And this will fire off the pending, success and error request with the format ${typePrefix}_PENDING, ${typePrefix}_SUCCESS, ${typePrefix}_ERROR, You can find the middleware here. But by all means just use whatever you feel best fits your use case, I felt like sharing this work because it's a frustration that brought me to build a middleware to handle it. Keep in mind that I made some assumptions on your case so if I am completely off let me know.
if I understand your question correctly, you want to be able to see if a separate class is loaded its data yet. If yes, then don't call the API to load the cats again.
There are two ways to do this, let's assumed COM1 and COM2 are your components.
return the entire state instead of just the specific variables you want for both of your components:
return state
then reference the cats in each component:
this.props.COM1.cats.items
this.props.COM2.cats.items
return the specific cats variable from the other components. you do the following for each components:
function mapStateToProps(state, ownProps) {
let cats = state.COM1.cats.items;
let loaded: state.cats.loaded;
let otherCats = state.COM2.cats.items;
return {
cats,
otherCats,
loaded
}
}

React-redux make a sync callback

i have this code:
that.props.getChats(that.props.state.chatModule.login.body.agent[0].company_id, that.props.state.chatModule.sessions.chatHashesChoosed)
that.scrollChatToBottom(sessionHash)
that.props.getChats is a function creator which use async ajax call.
i want that.scrollChatToBottom to run only after the ajax call return. the thing is that because i use this function creator, i dont get any promise or someting...
i dont want to make the call inside the component.
I guess getChats will retrieve some data and update the app state, which then trigger your component to be re-rendered according to the newly retrieved data? If so, you can trigger scrollChatToBottom in componentDidUpdate, e.g.
componentDidUpdate(prevProps, prevState) {
if (this.props.chatData !== NULL) { // assuming chatData is the data retrieved by getChats
// Do scrollChatToBottom here
}
}

Resources