Run action in loop until or finished with success - redux

I have an epic which is trigger when the user updates data about his video. The user has a single page with multiple videos on it and can do update title on any chosen video. At bottom of the page there is Submit and when the user clicks it I need to save changes to all video so I dispatch action saveVideo. This action is triggering epic which will send a request and on success should trigger next pending video on the list to be saved.
Epic:
const updateVideoEpic = action$ =>
action$.ofType(videoTypes.UPDATE_VIDEO_REQUEST)
.switchMap(action => Observable.from(updateVideo(action.payload)))
.takeUntil(action$.ofType(videoTypes.UPDATE_VIDEO_REQUEST_CANCEL))
.map(video => videoAction.updateVideoSuccess(video))
.catch((error) => Observable.of(videoAction.updateVideoFailure(error)));
How I can trigger asynchronous epic so after success updating data of 1st video will trigger again the same epic to update data for next video.
I thought about creating another epic but how you check if everything passed.
Sudo code:
const updateVideos = action$ =>
action$.ofTypes(videoTypes.UPDATE_VIDEO_BATCH_REQUEST)
.switchMap(action => action.payload.videos)
.map(video => videoAction.updateVideoBatchRequest(video));

I change a little bit of approach and decided to use only fetch and loop though requests:
// Import stylesheets
import './style.css';
import { fromPromise } from 'rxjs/observable/fromPromise';
import { from } from 'rxjs/observable/from';
import { mergeMap, catchError, switchMap } from 'rxjs/operators';
import { zip } from 'rxjs/observable/zip';
const urls = [
{ url: 'https://bbc.co.uk', name: 'BBC'},
{ url: 'https://google.com', name: 'Google'},
{ url: 'https://amazon.com', name: 'Amazon'}
];
const items$ = from(urls).pipe(
mergeMap(item => fetch(item.url, {mode: 'no-cors'}))
)
items$.subscribe(
url => console.log(url),
error => console.log(error),
() => console.log('Complete')
);
Working copy:
Working copy on stackblitz

Related

React-redux dispatch action onclick using hooks and redux toolkit

Fairly new to redux, react-redux, and redux toolkit, but not new to React, though I am shaky on hooks. I am attempting to dispatch an action from the click of a button, which will update the store with the clicked button's value. I have searched for how to do this high and low, but now I am suspecting I am thinking about the problem in React, without understanding typical redux patterns, because what I expect to be possible is just not done in the examples I have found. What should I be doing instead? The onclick does seem to capture the selection, but it is not being passed to the action. My goal is to show a dynamic list of buttons from data collected from an axios get call to a list of routes. Once a button is clicked, there should be a separate call to an api for data specific to that clicked button's route. Here is an example of what I currently have set up:
reducersRoutes.js
import { createSlice } from "#reduxjs/toolkit";
import { routesApiCallBegan } from "./createActionRoutes";
const slice = createSlice({
name: "routes",
initialState: {
selected: ''
},
{... some more reducers...}
routeSelected: (routes, action) => {
routes.selected = action.payload;
}
},
});
export default slice.reducer;
const { routeSelected } = slice.actions;
const url = '';
export const loadroutes = () => (dispatch) => {
return dispatch(
routesApiCallBegan({
url,
{...}
selected: routeSelected.type,
})
);
};
createActionRoutes.js
import { createAction } from "#reduxjs/toolkit";
{...some other actions...}
export const routeSelected = createAction("routeSelection");
components/routes.js:
import { useDispatch, useSelector } from "react-redux";
import { loadroutes } from "../store/reducersRoutes";
import { useEffect } from "react";
import { routeSelected } from "../store/createActionRoutes";
import Generic from "./generic";
const Routes = () => {
const dispatch = useDispatch();
const routes = useSelector((state) => state.list);
const selected = useSelector((state) => state.selected);
useEffect(() => {
dispatch(loadroutes());
}, [dispatch]);
const sendRouteSelection = (selection) => {
dispatch(routeSelected(selection))
}
return (
<div>
<h1>Available Information:</h1>
<ul>
{routes.map((route, index) => (
<button key={route[index]} className="routeNav" onClick={() => sendRouteSelection(route[0])}>{route[1]}</button>
))}
</ul>
{selected !== '' ? <Generic /> : <span>Data should go here...</span>}
</div>
);
};
export default Routes;
Would be happy to provide additional code if required, thanks!
ETA: To clarify the problem - when the button is clicked, the action is not dispatched and the value does not appear to be passed to the action, even. I would like the selection value on the button to become the routeSelected state value, and then make an api call using the routeSelected value. For the purpose of this question, just getting the action dispatched would be plenty help!
After writing that last comment, I may actually see a couple potential issues:
First, you're currently defining two different action types named routeSelected:
One is in the routes slice, generated by the key routeSelected
The other is in createActionRoutes.js, generated by the call to createAction("routeSelection").
You're importing the second one into the component and dispatching it. However, that is a different action type string name than the one from the slice - it's just 'routeSelection', whereas the one in the slice file is 'routes/routeSelected'. Because of that, the reducer logic in the slice file will never run in response to that action.
I don't think you want to have that separate createAction() call at all. Do export const { routeSelected } = slice.actions in the slice file, and dispatch that action in the component.
I'm also a little concerned about the loadroutes thunk that you have there. I see that you might have omitted some code from the middle, so I don't know all of what it's doing, but it doesn't look like it's actually dispatching actions when the fetched data is retrieved.
I'd recommend looking into using RTK's createAsyncThunk API to generate and dispatch actions as part of data fetching - see Redux Essentials, Part 5: Async Logic and Data Fetching for examples of that.

When to save the state tree?

The Redux manual says every reducer should be a pure function and even no API call should be made, I then curious to know, then, when should I get chance to save my App state tree to an external storage or the backend?
You can save your redux store using and action with the Redux Thunk middleware.
Lets say you want to want to save the store when the user clicks save. First, define an action to do the save:
actions/save.js
import fetch from 'isomorphic-fetch'
export const save = state => {
return () => {
fetch('/api/path/to/save', {
body: JSON.stringify(state),
headers: {
'content-type': 'application/json'
}
method: 'POST'
}
}
}
Then in your component:
components/SaveButton.js
import React from 'react'
import { connect } from 'react-redux';
import { save } from '../actions/save'
const SaveButton = props => {
let { onSave, state } = props
return <button onClick={onSave(state)}>Save</button>
}
const mapStateToProps = state => {
return {state}
}
const mapDispatchToProps = dispatch => {
return {
onSave: state => dispatch(save(state))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SaveButton)
You shouldn't do that as part of your reducer.
Instead, whenever you want to save some part of your state, you should dispatch an asynchronous action (with the help of middleware like redux-thunk) perhaps called SAVE_XYZ with it's payload being the part of the store you want to save.
dispatch(saveXYZ(data))
saveXYZ needs to be an async action creator that will dispatch the API call to persist your data, and handle the response accordingly.
const saveXYZ = payload => dispatch => {
dispatch(saveXYZPending());
return apiCallToStore(...)
.then(data => saveXYZDone())
.catch(err => saveXYZError());
}
You can read more on async actions and how to handle them.
Two basic approaches:
Use store.subscribe(callback), and write a callback that gets the latest state and persists it after some action has been dispatched
Write a middleware that persists the state when some condition is met
There's dozens of existing Redux store persistence libraries available that will do this work for you.

React Redux dispatch an action to another domain for authentication

I'm using the omniauth-github strategy and upon a button click I want to dispatch an action to another domain, (such as 'https://github.com/login/oauth/authorize'). When using dispatch this however does not work as the browser preflights my request and resonds with 'No 'Access-Control-Allow-Origin'. I can get this to work by using an and point to the url, which then will send the user back to my backend to authenticate the user get the token store it. But without dispatch, I have to send back the JWT token my site generates in query params, and since I am omitting my action creators and reducers, I cannot store it in localStorage. Is there any way to perform dispatch cross domain?
export const loginGitHub = () => {
return dispatch => {
fetch('https://github.com/login/oauth/authorize?client_id=...&scope=user',{
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
},
mode: 'cors'
})
.then(resp=>resp.json())
.then(data => {
debugger
})
}
}
You'll need to provide your redux store's dispatch method to this method for it to work, this is typically done by using mapDispatchToProps with redux's connect() method: https://github.com/reduxjs/react-redux/blob/master/docs/api.md
That's the typical flow, if for some reason you need to call this outside a component like before you mount your React app (but after you've initialized your redux store) something like this can work:
import { createStore } from 'redux'
const store = createStore();
export const loginGitHub = dispatch => {
return dispatch => {
fetch('https://github.com/login/oauth/authorize?client_id=...&scope=user',{
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
},
mode: 'cors'
})
.then(resp=>resp.json())
.then(data => {
debugger
})
}
}
loginGitHub(store.dispatch);
That's very much an anti pattern, and I'd recommend properly using mapDispatchToProps which requires
Creating a store
Wrapping your app in a provider and providing the previously created store as a prop to the provider.
Using connect() like so within your component:
import React, { Component } from 'react';
import { connect } from 'redux';
import { loginGitHub } from './wherever';
class ExampleComponent extends Component {
// whatever component methods you need
}
const mapDispatchToProps = dispatch => ({
loginGitHub: () => dispatch(logInGitHub())
})
export default connect(null, mapDispatchToProps)(ExampleComponent);
Then you'll be able to call loginGitHub with this.props.loginGitHub() within your component.

Can home_url() be dynamically passed to a theme built with React?

Testing out and building a WordPress theme in React I've typically seen the URL hard coded in a componentDidMount like:
class Home extends React.Component{
componentDidMount(){
const API_URL = 'https://foobar/wp-json/wp/v2/posts/';
this.serverRequest = $.get(API_URL, function (result) {
this.setState({
result:result,
loaded:true
});
}.bind(this));
}
}
export default Home;
but I was curious to know if there was a way in the functions.php -> Home.js I can pass home_url, in the example https://foobar, without having to manually modify Home.js?
Per querying the site with the paramters [wordpress][reactjs] I've been unable to find if this is asked. Can this be done or a way to pass the wp-json/wp/v2 to React dynamically?
Edit:
There appears to be some confusion in what I'm trying to do. If you were to use Axios with React instead of fetch, example from Using Axios with React, you'd have to use a GET to pull the WP_JSON from WordPress. The example link uses:
componentDidMount() {
axios.get(`https://jsonplaceholder.typicode.com/users`)
.then(res => {
const persons = res.data;
this.setState({ persons });
})
}
the URL, https://jsonplaceholder.typicode.com/users, is static and every time I were to load this theme to a new site, the URL will need to be changed. How can I change the present URL to match the theme's hosted URL dynamically so that if the domain were to change from:
https://jsonplaceholder.typicode.com/users
to
https://jsonwinners.typicode.com/users
I wouldn't have to do it manually.
Judging from what you're trying to achieve, i would rather use document.location.origin
however, to answer your question, you CAN get php to send a variable to javascript....
Method 1 (dirty) - custom script tag in the head
add_action('wp_head', function(){
echo '<script>var MY_OBJ.users_url = "'.home_url('/users').'";</script>';
});
and then in your js file...
componentDidMount() {
axios.get(MY_OBJ.users_url)
.then(res => {
const persons = res.data;
this.setState({ persons });
})
}
Method 2 - localized script
wp_enqueue_script( 'home-js', get_theme_file_uri('/assets/js/Home.js'), array(), '1.0', true );
$my_php_array = array(
'home_url' => home_url('')
'users_url' => home_url('/users')
);
wp_localize_script( 'home-js', 'MY_OBJ', $my_php_array );
and then in your js file...
componentDidMount() {
axios.get(MY_OBJ.users_url)
.then(res => {
const persons = res.data;
this.setState({ persons });
})
}

Hold epic in Redux Observable until action is received

I'm new to rxjs and redux-observables
I have two epics:
export const appInit = action$ =>
action$.ofType(actions.appInitRequest)
.take(1)
.switchMap(() => actions.fetchData())
and
export const navigation = ($action, {getState}) =>
$action.ofType(actions.locationChange)
.do(console.log)
App Init fires action to fetch data,
Is it possible to hold the navigation epic till the fetch data complete? so if navigation action received and fetch data didn't complete we'll wait till fetch data complete (dispatch action.fetchDataSuccess) and than continue the navigation epic flow?
I tried the following code but than every request after the fetchDataSuccess waits for new fetchDataSuccess
export const navigation = ($action, {getState}) =>
$action.ofType(actions.locationChange)
.switchMap(({payload: {pathname}}) =>
$action.ofType(action.fetchDataSuccess)
.take(1)
Try to use withLatestFrom:
export const navigation = ($action, {getState}) =>
$action.ofType(actions.locationChange)
.withLatestFrom(appInit)
.filter(([locationChangeEvent, appInitEvent]) => {
return /* check that app inited successfully */
})
.map(([locationChangeEvent]) => locationChangeEvent)
.do(console.log)

Resources