Binding redux state to an object - javascript

Is there an established way to bind a redux state to an object?
I want to do something like this:
MyApi.setStore(myReduxStore, 'stateVar')
I've played with passing various get/set actions and store listeners but it's a mess.
MyApi.getState = () => store.dispatch(getAction())
MyApi.setState = (state) => store.dispatch(setAction(state))
let currentState
store.subscribe(() => {
let previousState = currentState
currentState = store.getState().stateVar
if(previousState !== currentState) {
MyApi.stateListener(currentState)
}
})

The way to do api calls in redux is to use a middleware like redux-thunk or redux-saga. That way you separate the api call from redux and just dispatch an action when the result is ready.
Example of an API call from redux-saga readme:
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'
// worker Saga: will be fired on USER_FETCH_REQUESTED actions
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED", message: e.message});
}
}
/*
Starts fetchUser on each dispatched `USER_FETCH_REQUESTED` action.
Allows concurrent fetches of user.
*/
function* mySaga() {
yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}
/*
Alternatively you may use takeLatest.
Does not allow concurrent fetches of user. If "USER_FETCH_REQUESTED" gets
dispatched while a fetch is already pending, that pending fetch is cancelled
and only the latest one will be run.
*/
function* mySaga() {
yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}
export default mySaga;
Then your reducer will just set loading state to true on "USER_FETCH_REQUESTED", update the state on "USER_FETCH_SUCCEEDED" and set some error state on "USER_FETCH_FAILED".

Related

Race condition when updating redux state?

I'm using redux-saga with takeEvery which allows multiple actions to execute simultaneously:
export function* watchSaga() {
yield all([
takeEvery(actionTypes.FETCH_DATA, fetchDataSaga)
])
}
and the fetchDataSaga looks like:
export function* fetchDataSaga(action) {
yield put(actions.fetchDataStart())
try {
const response = yield axios.post(url, body)
yield put(actions.fetchDataSuccess(response.data))
} catch (error) {
yield put(actions.fetchDataFail(error.response.data.error))
}
}
in fetchDataSuccess, I'm adding a new field (or updating if exist) into my redux state object:
const initialState = {
myObj: {}
}
...
const fetchDataSuccess = (state, action) => {
const newKey = action.newKey
return {
...state,
myObj: {
...myObj,
[newKey]: {
...myObj[newKey],
timestamp: new Date().getTime()
}
}
}
}
So I make concurrent calls to the saga ,let's say 100 times:
yield all([
call(fetchDataSaga, action[0])
call(fetchDataSaga, action[1])
call(fetchDataSaga, action[2])
...
call(fetchDataSaga, action[99])
])
There should be 100 fields in myObj afterward.
My question is, am I going to get the latest state every time in the fetchDataSuccess reducer? Or will there be a race condition that one of the reducers read the old previous state, and if so, how can I prevent it?

Why does an awaited Redux action creator not await properly? (React Native)

In my React Native app, I have an action creator that dispatches an action to update the state in the Redux store. I call this action creator with 'await' and then do a setState(), but I noticed the Redux store is getting updated AFTER the setState is completed. My code looks like this:
updateFunction = async () => {
await this.props.actionCreator1();
this.setState({property1: this.props.property1});
}
I 'await'ed the action dispatch because I need the store to be updated before I setState(). I must be misunderstanding what gets returned etc when you dispatch an action, because the store does not get updated until after setState() runs. Can anyone explain why this could occur?
Thanks!
UPDATE
This is how I bind my Redux store to my component
const Component1 = connect(
state => ({
property1: state.Reducer1.property1
}),
dispatch => bindActionCreators({
actionCreator1: ActionCreators.actionCreator1
}, dispatch)
)(Component1)
This is where I create the action creator in actions.js
export const ActionCreators = {
actionCreator1: () => ({type: 'actionCreator1', property1: 'value'})
}
This is where I reduce the actions in reducer.js
const initialState = {
property1: undefined
}
export default (state = initialState, action) {
switch(action.type) {
case('actionCreator1'): {
return {
...state,
property1: action.property1
}
}
}
}
You are not supposed to use await on a function that doesn't return a Promise, to make it work either you can wrap your action creator so that it returns a promise then resolves to the value you want, or do a different approach not relying on async-await.
You can define your action creator like below:
export const ActionCreators = {
actionCreator1: () => new Promise(function (resolve, reject) {
return resolve({type: 'actionCreator1', property1: 'value'})
});
}
It will work if you added redux-thunk middle ware.
I'm not sure you are using React/Redux best practices. You are using action which changes the redux state but at the same time you are trying to use setState which updates the state of React. Moreover, your code does not execute how you expect it because your await function is not promise based.
This does implicitly responds to your question. Take the functions below as an example:
function something1(){
setTimeout(() => console.log('before'), 2000);
}
function something2(){
var result = new Promise(resolve => setTimeout(() => resolve(console.log('before')) ,2000));
return result;
}
function el(){
console.log('after')
}
updateFunction = async () => {
await something1(); // await something2();
el();
return 'done';
};
and try first to run updateFunction() in your console. Because function something1() is not promise based you will see that it will not await and the string 'after' will be printed first. Now inside your updateFunction() try to change await something1() with something2() in your console again. You will see that the rest of the el() function will await for function something2() to finish first and your console will print out the strings 'before' and then 'after'. Hope this clarifies things a bit.

How to wait for all actions(success/failure after request) to dispatch in redux-saga?

I have following actions:
export function createRequest(inputValues) {
return {
type: actions.PERSON_CREATE_REQUEST,
payload: {
inputValues
}
};
}
export function createSuccess(person) {
return {
type: actions.PERSON_CREATE_SUCCESS,
payload: {
person: person
}
};
}
export function createFailure(error) {
return {
type: actions.PERSON_CREATE_FAILURE,
payload: {
error
}
};
}
Saga:
export function* createPerson(action) {
try {
const data = yield call((async () => {
const person = action.payload.inputValues;
return await api.post(person);
}));
yield put(createSuccess(data));
} catch (error) {
yield put(createFailure(error));
}
}
export function* watchCreatePerson() {
yield takeEvery('PERSON_CREATE_REQUEST', createPerson);
}
export function* rootSaga() {
yield [
fork(watchCreatePerson),
];
}
Function that is called on form submit:
handleSubmit = async (event, values) => {
event.preventDefault();
await store.dispatch(action.createRequest(values));
if (this.props.error === null) {
store.dispatch(reset(this.props.form));
}
};
After await store.dispatch(action.createRequest(values)); I want to check if PERSON_CREATE_FAILURE or PERSON_CREATE_SUCCESS was dispatched. On PERSON_CREATE_FAILURE I set error in store that is mapped to props. But next line of code with if is called after PERSON_CREATE_REQUEST is dispatched. But I need to wait until PERSON_CREATE_FAILURE or PERSON_CREATE_SUCCESS dispatch. How to change saga to wait for that actions dispatches?
In the handleSubmit function, just dispatch the createRequest action, don't await it's response at this point in your code.
Now, if the request fails, write a reducer to handle the PERSON_CREATE_FAILURE action. Within this reducer you can then set the application state appropriately i.e. reset the form state. Then using MapStateToProps for your component which encompasses your form, you form will automatically re-render to match the updated (and in this case reset) form state.
Note that with this approach you will need to create your form's state in the Redux store, but doing so will simplify the application's state lifecyle.

How to get something from the state / store inside a redux-saga function?

How do I access the redux state inside a saga function?
Short answer:
import { select } from 'redux-saga/effects';
...
let data = yield select(stateSelectorFunction);
As #markerikson already says, redux-saga exposes a very useful API select() to invoke a selector on the state for getting some part of it available inside the saga.
For your example a simple implementation could be:
/*
* Selector. The query depends by the state shape
*/
export const getProject = (state) => state.project
// Saga
export function* saveProjectTask() {
while(true) {
yield take(SAVE_PROJECT);
let project = yield select(getProject); // <-- get the project
yield call(fetch, '/api/project', { body: project, method: 'PUT' });
yield put({type: SAVE_PROJECT_SUCCESS});
}
}
In addition to the suggested doc by #markerikson, there is a very good video tutorial by D. Abramov which explains how to use selectors with Redux. Check also this interesting thread on Twitter.
This is what "selector" functions are for. You pass them the entire state tree, and they return some piece of the state. The code that calls the selector doesn't need to know where in the state that data was, just that it was returned. See http://redux.js.org/docs/recipes/ComputingDerivedData.html for some examples.
Within a saga, the select() API can be used to execute a selector.
I used an eventChannel to dispatch an action from a callback within the generator function
import {eventChannel} from 'redux-saga';
import {call, take} from 'redux-saga/effects';
function createEventChannel(setEmitter) {
return eventChannel(emitter => {
setEmitter(emitter)
return () => {
}
}
)
}
function* YourSaga(){
let emitter;
const internalEvents = yield call(createEventChannel, em => emitter = em)
const scopedCallback = () => {
emitter({type, payload})
}
while(true){
const action = yield take(internalEvents)
yield put(action)
}
}

How to avoid duplicate API requests with Redux-Saga?

So far I like Redux better than other Flux implementations, and I'm using it to re-write our front end application.
The main struggling points that I'm facing:
Maintaining the status of API calls to avoid sending duplicate requests.
Maintaining relationships between records.
The first issue could be solved by keeping a status field in the sub-state of each type of data. E.g.:
function postsReducer(state, action) {
switch(action.type) {
case "FETCH_POSTS":
return {
...state,
status: "loading",
};
case "LOADED_POSTS":
return {
status: "complete",
posts: action.posts,
};
}
}
function commentsReducer(state, action) {
const { type, postId } = action;
switch(type) {
case "FETCH_COMMENTS_OF_POST":
return {
...state,
status: { ...state.status, [postId]: "loading" },
};
case "LOADED_COMMENTS_OF_POST":
return {
status: { ...state.status, [postId]: "complete" },
posts: { ...state.posts, [postId]: action.posts },
};
}
}
Now I can make a Saga for Posts and another one for Comments. Each of the Sagas knows how to get the status of requests. But that would lead to a lot of duplicate code soon (e.g. Posts, Comments, Likes, Reactions, Authors, etc).
I'm wondering if there is a good way to avoid all that duplicate code.
The 2nd issue comes to existence when I need to get a comment by ID from the redux store. Are there best practices for handling relationships between data?
Thanks!
redux-saga now has takeLeading(pattern, saga, ...args)
Version 1.0+ of redux-saga has takeLeading that spawns a saga on each action dispatched to the Store that matches pattern. After spawning a task once, it blocks until the spawned saga completes and then starts to listen for a pattern again.
Previously I implemented this solution from the owner of Redux Saga and it worked really well - I was getting errors from API calls sometimes being fired twice:
You could create a higher order saga for this, which would look something like this:
function* takeOneAndBlock(pattern, worker, ...args) {
const task = yield fork(function* () {
while (true) {
const action = yield take(pattern)
yield call(worker, ...args, action)
}
})
return task
}
and use it like this:
function* fetchRequest() {
try {
yield put({type: 'FETCH_START'});
const res = yield call(api.fetch);
yield put({type: 'FETCH_SUCCESS'});
} catch (err) {
yield put({type: 'FETCH_FAILURE'});
}
}
yield takeOneAndBlock('FETCH_REQUEST', fetchRequest)
In my opinion this way is far way more elegant and also its behaviour can be easily customized depending on your needs.
I had the exact same issue in my project.
I have tried redux-saga, it seems that it's really a sensible tool to control the data flow with redux on side effects. However, it's a little complex to deal with the real world problem such as duplicate requests and handling relationships between data.
So I created a small library 'redux-dataloader' to solve this problem.
Action Creators
import { load } from 'redux-dataloader'
function fetchPostsRequest() {
// Wrap the original action with load(), it returns a Promise of this action.
return load({
type: 'FETCH_POSTS'
});
}
function fetchPostsSuccess(posts) {
return {
type: 'LOADED_POSTS',
posts: posts
};
}
function fetchCommentsRequest(postId) {
return load({
type: 'FETCH_COMMENTS',
postId: postId
});
}
function fetchCommentsSuccess(postId, comments) {
return {
type: 'LOADED_COMMENTS_OF_POST',
postId: postId,
comments: comments
}
}
Create side loaders for request actions
Then create data loaders for 'FETCH_POSTS' and 'FETCH_COMMENTS':
import { createLoader, fixedWait } from 'redux-dataloader';
const postsLoader = createLoader('FETCH_POSTS', {
success: (ctx, data) => {
// You can get dispatch(), getState() and request action from ctx basically.
const { postId } = ctx.action;
return fetchPostsSuccess(data);
},
error: (ctx, errData) => {
// return an error action
},
shouldFetch: (ctx) => {
// (optional) this method prevent fetch()
},
fetch: async (ctx) => {
// Start fetching posts, use async/await or return a Promise
// ...
}
});
const commentsLoader = createLoader('FETCH_COMMENTS', {
success: (ctx, data) => {
const { postId } = ctx.action;
return fetchCommentsSuccess(postId, data);
},
error: (ctx, errData) => {
// return an error action
},
shouldFetch: (ctx) => {
const { postId } = ctx.action;
return !!ctx.getState().comments.comments[postId];
},
fetch: async (ctx) => {
const { postId } = ctx.action;
// Start fetching comments by postId, use async/await or return a Promise
// ...
},
}, {
// You can also customize ttl, and retry strategies
ttl: 10000, // Don't fetch data with same request action within 10s
retryTimes: 3, // Try 3 times in total when error occurs
retryWait: fixedWait(1000), // sleeps 1s before retrying
});
export default [
postsLoader,
commentsLoader
];
Apply redux-dataloader to redux store
import { createDataLoaderMiddleware } from 'redux-dataloader';
import loaders from './dataloaders';
import rootReducer from './reducers/index';
import { createStore, applyMiddleware } from 'redux';
function configureStore() {
const dataLoaderMiddleware = createDataLoaderMiddleware(loaders, {
// (optional) add some helpers to ctx that can be used in loader
});
return createStore(
rootReducer,
applyMiddleware(dataLoaderMiddleware)
);
}
Handle data chain
OK, then just use dispatch(requestAction) to handle relationships between data.
class PostContainer extends React.Component {
componentDidMount() {
const dispatch = this.props.dispatch;
const getState = this.props.getState;
dispatch(fetchPostsRequest()).then(() => {
// Always get data from store!
const postPromises = getState().posts.posts.map(post => {
return dispatch(fetchCommentsRequest(post.id));
});
return Promise.all(postPromises);
}).then() => {
// ...
});
}
render() {
// ...
}
}
export default connect(
state => ()
)(PostContainer);
NOTICE The promised of request action with be cached within ttl, and prevent duplicated requests.
BTW, if you are using async/await, you can handle data fetching with redux-dataloader like this:
async function fetchData(props, store) {
try {
const { dispatch, getState } = store;
await dispatch(fetchUserRequest(props.userId));
const userId = getState().users.user.id;
await dispatch(fetchPostsRequest(userId));
const posts = getState().posts.userPosts[userId];
const commentRequests = posts.map(post => fetchCommentsRequest(post.id))
await Promise.all(commentRequests);
} catch (err) {
// error handler
}
}
First, you can create a generic action creator for fetching post.
function fetchPost(id) {
return {
type: 'FETCH_POST_REQUEST',
payload: id,
};
}
function fetchPostSuccess(post, likes, comments) {
return {
type: 'FETCH_POST_SUCCESS',
payload: {
post,
likes,
comments,
},
};
}
When you call this fetch post action, it'll trigger onFetchPost saga.
function* watchFetchPost() {
yield* takeLatest('FETCH_POST_REQUEST', onFetchPost);
}
function* onFetchPost(action) {
const id = action.payload;
try {
// This will do the trick for you.
const [ post, likes, comments ] = yield [
call(Api.getPost, id),
call(Api.getLikesOfPost, id),
call(Api.getCommentsOfPost, id),
];
// Instead of dispatching three different actions, heres just one!
yield put(fetchPostSuccess(post, likes, comments));
} catch(error) {
yield put(fetchPostFailure(error))
}
}

Categories

Resources