I've got this working but i'm after a more 'best practice way'.
its using the https://icanhazdadjoke api to display a random joke that gets updated every x seconds. is there a better way of doing this?
eventually i want to add stop, start, reset functionality and feel this way might not be the best.
Any middleware i can use?
Redux actions
// action types
import axios from 'axios';
export const FETCH_JOKE = 'FETCH_JOKE';
export const FETCH_JOKE_SUCCESS = 'FETCH_JOKE_SUCCESS';
export const FETCH_JOKE_FAILURE = 'FETCH_JOKE_FAILURE';
function fetchJoke() {
return {
type: FETCH_JOKE
};
}
function fetchJokeSuccess(data) {
return {
type: FETCH_JOKE_SUCCESS,
data
};
}
function fetchJokeFail(error) {
return {
type: FETCH_JOKE_FAILURE,
error
};
}
export function fetchJokeCall(){
return function(dispatch){
dispatch(fetchJoke());
return axios.get('https://icanhazdadjoke.com', { headers: { 'Accept': 'application/json' }})
.then(function(result){
dispatch(fetchJokeSuccess(result.data))
})
.catch(error => dispatch(fetchJokeFail(error)));
}
}
Redux reducer
import {combineReducers} from 'redux';
import {FETCH_JOKE, FETCH_JOKE_SUCCESS, FETCH_JOKE_FAILURE} from '../actions';
const defaultStateList = {
isFetching: false,
items:[],
error:{}
};
const joke = (state = defaultStateList, action) => {
switch (action.type){
case FETCH_JOKE:
return {...state, isFetching:true};
case FETCH_JOKE_SUCCESS:
return {...state, isFetching:false, items:action.data};
case FETCH_JOKE_FAILURE:
return {...state, isFetching:false, error:action.data};
default:
return state;
}
};
const rootReducer = combineReducers({
joke
});
export default rootReducer;
Joke component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { fetchJokeCall } from '../actions';
class Joke extends Component {
componentDidMount() {
this.timer = setInterval(()=> this.props.fetchJokeCall(), 1000);
}
componentWillUnmount() {
clearInterval(this.timer)
this.timer = null;
}
render() {
return (
<div>
{this.props.joke.joke}
</div>
);
}
}
Joke.propTypes = {
fetchJokeCall: PropTypes.func,
joke: PropTypes.array.isRequired
};
function mapStateToProps(state) {
return {
joke: state.joke.items,
isfetching: state.joke.isFetching
};
}
export default connect(mapStateToProps, { fetchJokeCall })(Joke);
Redux-Sagas is better and we are using it in our applications as well, this is how you can poll using Redux-Sagas
Just to give you an idea this is how you can do it, You also need to understand how Redux-Sagas work
Action
export const FETCH_JOKE = 'FETCH_JOKE';
export const FETCH_JOKE_SUCCESS = 'FETCH_JOKE_SUCCESS';
export const FETCH_JOKE_FAILURE = 'FETCH_JOKE_FAILURE';
export const START_POLLING = 'START_POLLING';
export const STOP_POLLING = 'STOP_POLLING';
function startPolling() {
return {
type: START_POLLING
};
}
function stopPolling() {
return {
type: STOP_POLLING
};
}
function fetchJoke() {
return {
type: FETCH_JOKE
};
}
function fetchJokeSuccess(data) {
return {
type: FETCH_JOKE_SUCCESS,
data
};
}
function fetchJokeFail(error) {
return {
type: FETCH_JOKE_FAILURE,
error
};
}
Reducer
import {combineReducers} from 'redux';
import {FETCH_JOKE, FETCH_JOKE_SUCCESS, FETCH_JOKE_FAILURE, START_POLLING, STOP_POLLING } from '../actions';
const defaultStateList = {
isFetching: false,
items:[],
error:{},
isPolling: false,
};
const joke = (state = defaultStateList, action) => {
switch (action.type){
case FETCH_JOKE:
return {...state, isFetching:true};
case FETCH_JOKE_SUCCESS:
return {...state, isFetching:false, items:action.data};
case FETCH_JOKE_FAILURE:
return {...state, isFetching:false, error:action.data};
case START_POLLING:
return {...state, isPolling: true};
case STOP_POLLING:
return {...state, isPolling: false};
default:
return state;
}
};
const rootReducer = combineReducers({
joke
});
export default rootReducer;
Sagas
import { call, put, takeEvery, takeLatest, take, race } from 'redux-saga/effects'
import {FETCH_JOKE, FETCH_JOKE_SUCCESS, FETCH_JOKE_FAILURE, START_POLLING, STOP_POLLING } from '../actions';
import axios from 'axios';
function delay(duration) {
const promise = new Promise(resolve => {
setTimeout(() => resolve(true), duration)
})
return promise
}
function* fetchJokes(action) {
while (true) {
try {
const { data } = yield call(() => axios({ url: ENDPOINT }))
yield put({ type: FETCH_JOKE_SUCCESS, data: data })
yield call(delay, 5000)
} catch (e) {
yield put({ type: FETCH_JOKE_FAILURE, message: e.message })
}
}
}
function* watchPollJokesSaga() {
while (true) {
const data = yield take(START_POLLING)
yield race([call(fetchJokes, data), take(STOP_POLLING)])
}
}
export default function* root() {
yield [watchPollJokesSaga()]
}
You can also use Redux-Observable, if you want to get more into this read this article
I've been working on pretty much the same problem, except that I wasn't concerned about starting and stopping the poll. For some reason the while loop kept freezing my app so I dispensed of it and instead set up my saga like this.
import { all, takeLatest, call, put } from 'redux-saga/effects';
import axios from 'axios';
import { API_CALL_REQUEST, API_CALL_SUCCESS, API_CALL_FAILURE, API_CALL_FETCHED } from
'../actions/giphy';
function apiFetch() {
let randomWord = require('random-words');
let API_ENDPOINT = `https://api.giphy.com/v1/gifs/search?
api_key=MYKEY&q=${randomWord()}&limit=12`;
return axios({
method: "get",
url: API_ENDPOINT
});
}
export function* fetchImages() {
try {
const res = yield call(apiFetch)
const images = yield res.data
yield put({type: API_CALL_SUCCESS, images})
} catch (e) {
yield put({type: API_CALL_FAILURE, e})
console.log('Error fetching giphy data')
}
}
export default function* giphySaga() {
yield all([
takeLatest(API_CALL_REQUEST, fetchImages),
]);
}
Then inside my component I added this.
componentDidMount() {
this.interval = setInterval(() => {
this.props.dispatch({type: 'API_CALL_REQUEST'});
}, 5000);
}
componentWillUnmount() {
clearInterval(this.interval)
}
It's working, but would like some feedback on how this could be possibly improved.
Here's a poor man's way. I don't think it's the best way but it doesn't require any extra library.
Actions
// action types
import axios from 'axios';
export const START_POLLING_JOKE = 'START_POLLING_JOKE';
export const STOP_POLLING_JOKE = 'STOP_POLLING_JOKE';
export const FETCH_JOKE = 'FETCH_JOKE';
export const FETCH_JOKE_SUCCESS = 'FETCH_JOKE_SUCCESS';
export const FETCH_JOKE_FAILURE = 'FETCH_JOKE_FAILURE';
const defaultPollingInterval = 60000
function startPollingJoke(interval = defaultPollingInterval) {
return function (dispatch) {
const fetch = () => dispatch(fetchJoke())
dispatch({
type: START_POLLING_JOKE,
interval,
fetch,
})
}
}
function stopPollingJoke() {
return {
type: STOP_POLLING_JOKE
}
}
function fetchJoke() {
return {
type: FETCH_JOKE
};
}
function fetchJokeSuccess(data) {
return {
type: FETCH_JOKE_SUCCESS,
data
};
}
function fetchJokeFail(error) {
return {
type: FETCH_JOKE_FAILURE,
error
};
}
export function pollJokeCall(interval = defaultPollingInterval) {
return function (dispatch) {
dispatch(fetchJoke())
dispatch(startPollingJoke(interval))
}
}
export function fetchJokeCall() {
return function(dispatch){
dispatch(fetchJoke());
return axios.get('https://icanhazdadjoke.com', { headers: { 'Accept': 'application/json' }})
.then(function(result){
dispatch(fetchJokeSuccess(result.data))
})
.catch(error => dispatch(fetchJokeFail(error)));
}
}
Reducers
import {combineReducers} from 'redux';
import {
START_POLLING_JOKE,
STOP_POLLING_JOKE,
FETCH_JOKE,
FETCH_JOKE_SUCCESS,
FETCH_JOKE_FAILURE,
} from '../actions';
const defaultStateList = {
isFetching: false,
items:[],
error:{},
pollingId: null,
polling: false,
};
const joke = (state = defaultStateList, action) => {
switch (action.type){
case START_POLLING_JOKE:
clearInterval(state.pollingId)
return {
...state,
polling: true,
pollingId: setInterval(action.fetch, action.interval),
}
}
case STOP_POLLING_JOKE:
clearInterval(state.pollingId)
return {...state, polling: false, pollingId: null}
case FETCH_JOKE:
return {...state, isFetching:true};
case FETCH_JOKE_SUCCESS:
return {...state, isFetching:false, items:action.data};
case FETCH_JOKE_FAILURE:
return {...state, isFetching:false, error:action.data};
default:
return state;
}
};
const rootReducer = combineReducers({
joke
});
export default rootReducer;
Component (might have a bug because I'm not used to class components)
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { pollJokeCall, stopPollingJoke } from '../actions';
class Joke extends Component {
componentDidMount() {
this.props.pollJokeCall()
}
componentWillUnmount() {
this.props.stopPollingJoke()
}
render() {
return (
<div>
{this.props.joke.joke}
</div>
);
}
}
Joke.propTypes = {
pollJokeCall: PropTypes.func,
stopPollingJoke: PropTypes.func,
joke: PropTypes.array.isRequired,
};
function mapStateToProps(state) {
return {
joke: state.joke.items,
isfetching: state.joke.isFetching
};
}
export default connect(mapStateToProps, { pollJokeCall, stopPollingJoke })(Joke);
I have made a small (5kb gzipped) helper to create polling based on redux-thunk store. The idea is to have a logic to prevent registering the same polling twice, have callbacks between iterations and more.
https://www.npmjs.com/package/redux-polling-thunk
redux-saga is great and I've been using this with redux. It provides a great api to do things like delay, polling, throttling, race conditions, task cancellations. So using redux-saga, you can add a watcher whcih will keep on pooling
function* pollSagaWorker(action) {
while (true) {
try {
const { data } = yield call(() => axios({ url: ENDPOINT }));
yield put(getDataSuccessAction(data));
yield call(delay, 4000);
} catch (err) {
yield put(getDataFailureAction(err));
}
}
}
I have read through 100's of these threads on here, and I can't seem to understand why my component isn't updating. I am pretty sure it has something to do with the Immutability, but I can't figure it out.
The call is being made, and is returning from the server. The state is changing (based on the redux-Dev-Tools that I have installed).I have made sure to not mutate the state in any instance, but the symptoms seem to point that direction.
Code Sandbox of whole app https://codesandbox.io/s/rl7n2pmpj4
Here is the component.
class RetailLocationSelector extends Component {
componentWillMount() {
this.getData();
}
getData = () => {
this.props.getRetailLocations()
}
render() {
const {data, loading} = this.props;
return (
<div>
{loading
? <LinearProgress/>
: null}
<DefaultSelector
options={data}
placeholder="Retail Location"/>
</div>
);
}
}
function mapStateToProps(state) {
return {
loading: state.retaillocations.loading,
data: state.retaillocations.data,
osv: state.osv};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
getRetailLocations,
selectRetailLocation,
nextStep
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(RetailLocationSelector);
And here is my reducer :
import {REQUEST_RETAIL_LOCATIONS, SUCCESS_RETAIL_LOCATIONS,
ERR_RETAIL_LOCATIONS, SELECT_RETAIL_LOCATION} from
'../actions/RetailLocationsAction'
const initialState = {
data: [],
loading: false,
success: true,
selectedRetailLocation: undefined
}
function retailLocation(state = initialState, action) {
switch (action.type) {
case REQUEST_RETAIL_LOCATIONS:
return Object.assign({}, state, {
loading: true
}, {success: true})
case SUCCESS_RETAIL_LOCATIONS:
return Object.assign({}, state, {
loading: false
}, {
success: true
}, {
data: Object.assign([], action.payload.data)
})
case ERR_RETAIL_LOCATIONS:
return Object.assign({}, state, {
loading: false
}, {
success: false
}, {errorMsg: action.payload.message})
case SELECT_RETAIL_LOCATION:
return Object.assign({}, state, {
selectedRetailLocation: state
.data
.find((rec) => {
return rec.id === action.payload.id
})
})
default:
return state;
}
}
export default retailLocation
And finally, my Action file:
import axios from 'axios';
//import {api} from './APIURL'
export const REQUEST_RETAIL_LOCATIONS = 'REQUEST_RETAIL_LOCATIONS'
export const SUCCESS_RETAIL_LOCATIONS = 'SUCCESS_RETAIL_LOCATIONS'
export const ERR_RETAIL_LOCATIONS = 'ERR_RETAIL_LOCATIONS'
export const SELECT_RETAIL_LOCATION = 'SELECT_RETAIL_LOCATION'
const URL = 'localhost/api/v1/retail/locations?BusStatus=O&LocType=C'
export const getRetailLocations = () => (dispatch) => {
dispatch({ type: 'REQUEST_RETAIL_LOCATIONS' });
return axios.get(URL)
.then(data => dispatch({ type: 'SUCCESS_RETAIL_LOCATIONS', payload: data }))
.catch(error => dispatch({type : 'ERR_RETAIL_LOCATIONS', payload: error}));
}
Combined Reducer
import { combineReducers } from "redux";
import retailLocations from './RetailLocationsReducer'
import vendors from './VendorsReducer'
import receiptInformation from './ReceiptInfoReducer'
import osv from './OSVReducer'
import receiptDetail from './ReceiptDetailReducer'
const allReducers = combineReducers({
retaillocations: retailLocations,
vendors: vendors,
receiptInformation: receiptInformation,
receiptDetail: receiptDetail,
osv: osv
});
export default allReducers;
This answer doesn't solve your issue totally but provides some hints about what is not working. The broken part is your store definition. I don't have much experience with redux-devtools-extension or redux-batched-subscribe but if you define your store like that your thunk function works:
const store = createStore(reducer, applyMiddleware(thunk));
One of the configuration for the mentioned packages above brokes your thunk middleware.
I am trying to get a simple react-redux app to work and I am running into a weird error that I can't figure out. I am trying to simply set my current user's first name and handle the store and one set function works and the other doesn't.
setCurrentUserFirstName - works
setCurrentUserHandle - doesn't
import React, { Component } from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import store from '../../store';
var Utilities = require('../../../common/commonutilities.js');
var RestClient = require('../../../common/restClient.js');
//actions
import { setCurrentUserHandle, setCurrentUserFirstName } from '../../actions/userActions';
class Header extends Component {
constructor(props) {
super(props);
this.state = {};
RestClient.api.fetchGet('/getcurrentuser', (response) => {
if(response.success) {
this.setState({
isAuthenticated: true,
currentUser: response.currentUser
});
store.dispatch({
type: 'USER_DID_LOGIN',
userLoggedIn: true
});
//works fine
this.props.setCurrentUserFirstName(response.currentUser.firstName);
//doesn't work and throws the error: "TypeError: _this.props.setCurrentUserHandle is not a function"
this.props.setCurrentUserHandle(response.currentUser.handle);
}
},
(err) => {
console.log(err);
});
}
render() {
return (
{this.props.user.currentUserFirstName}, {this.props.user.currentUserHandle}
);
}
}
const mapStateToProps = function(store) {
return {
//user properties
user: store.userState
};
};
const mapDispatchToProps = (dispatch) => {
return{
setCurrentUserFirstName: (currentUserFirstName) =>{
dispatch( setCurrentUserFirstName(currentUserFirstName));
}
}
return{
setCurrentUserHandle: (currentUserHandle) =>{
dispatch( setCurrentUserHandle(currentUserHandle));
}
}
};
//connect it all
export default connect(mapStateToProps, mapDispatchToProps)(Header);
I have them as actions in the userActions.js file
export function setCurrentUserFirstName(currentUserFirstName){
return{
type: 'SET_CURRENT_USER_FIRST_NAME',
payload: currentUserFirstName
};
}
export function setCurrentUserHandle(currentUserHandle){
return{
type: 'SET_CURRENT_USER_HANDLE',
payload: currentUserHandle
};
}
And in the reducer
const initialUserState = {
user: {},
currentUserFirstName:[],
currentUserHandle:[]
};
// The User reducer
const userReducer = (state = initialUserState, action) => {
//using newState object to be immutable
let newState = state;
switch (action.type) {
case 'SET_CURRENT_USER_FIRST_NAME':
newState = {
...state,
currentUserFirstName: action.payload
};
break;
case 'SET_CURRENT_USER_HANDLE':
newState = {
...state,
currentUserHandle: action.payload
};
break;
break;
default:
break;
}
return newState;
};
export default userReducer;
What do I have incorrect?
You have 2 return statements in your mapDispatchToProps - the second one will never be reached. You can return a single object as follows:
const mapDispatchToProps = (dispatch) => {
return{
setCurrentUserFirstName: (currentUserFirstName) =>{
dispatch( setCurrentUserFirstName(currentUserFirstName));
},
setCurrentUserHandle: (currentUserHandle) =>{
dispatch( setCurrentUserHandle(currentUserHandle));
}
}
};
In addition to Tony's correct answer, I highly encourage that you use the "object shorthand" form of mapDispatch instead. You can pass an object full of action creators as the second argument to connect(), instead of an actual mapDispatch function. In your case, it'd look like:
import { setCurrentUserHandle, setCurrentUserFirstName } from '../../actions/userActions';
const actionCreators = { setCurrentUserHandle, setCurrentUserFirstName };
class Header extends Component {}
export default connect(mapStateToProps, actionCreators)(Header);
I'm trying to make an api call in action with axios and pass the results of it to the reducer.
Though action is triggered, reducer isn't. And I can't understand why.
Here's the component that should make api call before mounting
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
//actions
import { getPost } from '../actions/';
class PostShow extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
getPost(this.props.params.id);
}
render() {
console.log(this.props.activePost);
return (
<div>
<h1> hello from a post</h1>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
activePost: state.posts.activePost
}
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
getPost
}, dispatch);
};
export default connect(mapStateToProps, mapDispatchToProps)(PostShow);
Here's my action
import axios from 'axios';
import { FETCH_POSTS, SEND_POST, FETCH_POST } from './types';
const ROOT_URL = 'http://reduxblog.herokuapp.com/api';
const API_KEY = '?key=qwerty';
export function fetchPosts() {
const req = axios.get(`${ROOT_URL}/posts${API_KEY}`);
return {
type: FETCH_POSTS,
payload: req
}
}
export function sendPost(props) {
const req = axios.post(`${ROOT_URL}/posts${API_KEY}`, props);
return {
type: SEND_POST,
payload: req
}
}
export function getPost(id) {
console.log('action triggered');
const req = axios.get(`${ROOT_URL}/posts/${id}${API_KEY}`);
return {
type: FETCH_POST,
payload: req
}
}
And here's my reducer
import { FETCH_POSTS, FETCH_POST } from '../actions/types';
const INITIAL_STATE = {
allPosts: [],
activePost: null
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case FETCH_POSTS:
return {
...state,
allPosts: action.payload.data
};
case FETCH_POST:
console.log('reducer triggered');
return {
...state,
activePost: action.payload.data
};
default:
return state;
}
}
As a result I see 'action triggered' coming from console.log in action, and null coming from console.log in component, and no console.log from the reducer, so it's not triggered and I have no data to render in my component.
Though I make a request and get a response from server with the data, it doesn't go to the reducer. (moreover case FETCH_POSTS works fine and I can render a list of posts, but not a particular one).
"axios": "^0.17.0"
"redux-promise": "^0.5.3"
You need to use this.props.getPost in componentDidMount instead of getPost.
Connect sends bound action creator to component as a prop
I want to delete a post. I'm using redux-promise as middleware.
My action looks like this:
export function deletePost(id) {
const request = axios.delete(`${ROOT_URL}/${id}?apiKey=${API_KEY}`)
return {
type: DELETE_POST,
payload: request
}
}
Then I have a button in my component to trigger action.
onDeleteClick() {
deletePost(id)
.then(setState({ redirect: true }))
}
The problem is that I can't use 'then()'. I would simply like to redirect user to homepage after deleting post.
Please help me guys.
Source code on request.
actions/index.js
import axios from 'axios';
export const DELETE_POST = 'DELETE_POST';
export const ROOT_URL = 'example.com';
export const API_KEY = 'randomstring';
export function deletePost(id) {
const request = axios.delete(`${ROOT_URL}/${id}?apiKey=${API_KEY}`)
return {
type: DELETE_POST,
payload: request
}
}
reducers/post_reducer.js
import { DELETE_POST } from '../actions/index';
const INITIAL_STATE = { all: [], post: null };
export default function(state = INITIAL_STATE, action) {
switch(action.type) {
return state.all.filter(post => post !== action.payload.data);
default:
return state;
}
}
components/PostShow.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchPost, deletePost } from '../actions/index';
import { Link } from 'react-router-dom';
import { Redirect } from 'react-router';
class PostsShow extends Component {
constructor(props) {
super(props)
this.state = {
redirect: false
}
}
componentWillMount() {
this.props.fetchPost(this.props.match.params.id)
}
onDeleteClick() {
deletePost(this.props.match.params.id)
.then(() => this.setState({ redirect: true }))
}
render() {
if(!this.props.post) {
return <div>Loading...</div>
}
if(this.state.redirect) {
return <Redirect to='/'/>
}
return (
<div className='blog-post container'>
<h3>{this.props.post.title}</h3>
<h6>Categories: {this.props.post.categories}</h6>
<p>{this.props.post.content}</p>
<Link to='/'><button className='btn btn-primary'>Back</button></Link>
{ this.props.user
? <button className='btn btn-danger' onClick={this.onDeleteClick.bind(this)}>Delete</button>
: null }
</div>
);
}
}
function mapStateToProps(state) {
return {
post: state.posts.post,
user: state.user
}
}
export default connect(mapStateToProps, { fetchPost, deletePost }) (PostsShow)
Per the redux-promise source code, it looks like it should return the chained promise:
return isPromise(action.payload)
? action.payload.then(
result => dispatch({ ...action, payload: result }),
error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
}
)
: next(action);
So, I would assume that you could chain off of the dispatch.
That said, your snippet has a couple potential issues. If that's in a component, then where is deletePost coming from? You're also not using this in front of setState. Assuming that deletePost is a bound-up action creator, this example should be correct:
onDeleteClick() {
this.props.deletePost(id)
.then(() => this.setState({redirect : true});
}