I am trying to use redux-saga to call my API and passing it a parameter. I am using redux-logger and console.log() to debug. But it seems that my saga doesn't go call the API for some reason.
Here is the console log of the states... as you can see the apiMsg doesnt change, however I have it change for each action.
I think I am going wrong somewhere in the actions, reducer or the component. Or else I am calling the API wrong in the Saga.
This is my code
api.js
import axios from 'axios';
export function getPostApi(postId){
return axios.get(
`https://jsonplaceholder.typicode.com/posts/${postId}`
).then(
response => {
return response;
}
).catch(
err => {throw err;}
);
}
my store
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga'
import {createLogger} from 'redux-logger';
import rootReducer from '../Reducers';
export default function configureStore(initialState) {
const sagaMiddleware = createSagaMiddleware();
const logger = createLogger();
return {
...createStore(rootReducer, initialState, applyMiddleware(sagaMiddleware, logger)),
runSaga: sagaMiddleware.run
}
}
the saga
import {call, put, takeEvery} from 'redux-saga/effects';
import * as service from '../Services/api';
import * as actions from '../Actions';
import * as types from '../actions/actionTypes';
export function* fetchPost(action){
try{
yield put(actions.apiRequest());
const [post] = yield [call(service.getPostApi, action.postId)];
yield put(actions.apiRequestSucceeded(post));
} catch(e){
yield put(actions.apiRequestFailed());
}
}
export function* watchApiRequest() {
yield takeEvery(types.FETCH_POST, fetchPost);
}
actions
import * as types from './actionTypes';
export const fetchPost = (postId) => ({
type: types.FETCH_POST,
postId,
});
export const apiRequest = () => {
return {
type: types.API_REQUEST
}
}
export const apiRequestSucceeded = (post) => {
return {
type: types.API_REQUEST_SUCCEEDED,
post
}
}
export const apiRequestFailed = () => {
return {
type: types.API_REQUEST_FAILED
}
}
reducer
import * as types from '../actions/actionTypes';
const initialState = {
apiMsg: '',
postId: 1,
fetching: false,
post: null
};
export default function getPostData(state = initialState, action) {
switch (action.type) {
case types.API_REQUEST:
return {
...state,
apiMsg: 'API call request!',
fetching: true
}
case types.API_REQUEST_SUCCEEDED:
return {
...state,
apiMsg: 'API called succeeded!',
fetching: false,
postId: action.post.id,
post: action.title
};
case types.API_REQUEST_FAILED:
return {
...state,
apiMsg: 'API called failed!',
fetching: false,
};
default:
return state;
}
}
the component page
import React, { Component } from 'react';
import { View, Text} from 'react-native';
import { connect } from 'react-redux';
import {fetchPost} from './Actions/apiTesterActions';
class TestPage extends Component {
constructor() {
super();
}
componentDidMount() {
this.props.fetchPost(1)
}
render() {
const { postId, fetching, post } = this.props;
console.log('Test page props', postId, fetching, post);
return (
<View>
<Text>Test Page Works! </Text>
</View>
)
}
}
const mapStateToProps = (state) => {
return {
postId: state.getPostData.postId,
fetching: state.getPostData.fetching,
post: state.getPostData.post
}
};
export default connect(mapStateToProps, {fetchPost})(TestPage);
I am still learning Redux, and now redux-saga, so I don't fully understand the structure/hierarchy, but I am learning and any advice is helpful. Thank you
EDIT:
I am using this redux saga api call example as a guide.
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { Provider } from 'react-redux';
import configureStore from './ReduxTest/Store/configureStore';
import rootSaga from './ReduxTest/sagas';
const store = configureStore();
store.runSaga(rootSaga)
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));
registerServiceWorker();
saga/index.js
import {fork, all} from 'redux-saga/effects';
import {watchApiRequest} from './apiTester';
export default function* rootSaga() {
yield all [
fork(watchApiRequest)
]
}
After some time with a lot of debugging, I have figured the problem.
When I first started creating the application, I got the following error...
[...effects] has been deprecated in favor of all([...effects]), please update your code
As the yield all [...] was deprecated, I changed my code without taking it into consideration, I was happy the error went, but didn't realize how much of an impact it would have on my application.
(wrong) saga/index.js
import {fork, all} from 'redux-saga/effects';
import {watchApiRequest} from './apiTester';
export default function* rootSaga() {
yield all [
fork(watchApiRequest)
]
}
(right) saga/index.js
import {fork, all} from 'redux-saga/effects';
import {watchApiRequest} from './apiTester';
export default function* rootSaga() {
yield all ([
fork(watchApiRequest)
])
}
As You can see it needed to be wrapped in brackets yield all ([...])
You will get the deprecated error but your code will still work. I am unsure how to fix the deprecated error.
If someone knows how to get rid of the deprecated error than that would great.
Related
I am using saga.js with Redux in my project and I am trying to call an API but that API is not being called. The generator function is called, but with yield.put() other method is not being called. I am fairly new to Redux Saga and I am stuck here. Any help would be really appreciated.
Saga.js
import { put, takeEvery, all ,fork, takeLatest} from "redux-saga/effects";
import axios from "axios";
function* runOurAction() {
let remoteData;
yield axios.get(url).then((resp) => {
remoteData = resp.data;
});
yield put({ type: "SET_DATA", payload: remoteData });
};
function* getAsyncDataWatcher() {
yield takeLatest("GET_TEAMS", runOurAction);
}
export default function* rootSaga() {
yield fork(getAsyncDataWatcher)
}
getAsyncDataWatcher() is being called but its not calling runOurAction
Reducer.js
const teams=(state=[],action)=>{
switch(action.type) {
case "SAVE_TEAMS":
return { ...state, payload: action.payload };
case "GET_TEAMS":
return { ...state, payload: action.payload };
case "SET_DATA":
return { ...state, payload: action.payload };
default:
return state;
}
}
export default teams;
Actions.js
const getTeams = (payload) => {
return {
type: "GET_TEAMS",
payload:payload
};
};
const saveTeams = (payload) => {
return {
type: "SAVE_TEAMS",
payload:payload
};
};
export { saveTeams, getTeams };
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import rootReducer from "./reducers";
import createSagaMiddleware from "redux-saga";
import { createStore, applyMiddleware ,compose} from "redux";
import { Provider } from "react-redux";
import rootSaga from "./saga";
const sagaMiddleware = createSagaMiddleware();
const enhancers = [window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), applyMiddleware(sagaMiddleware)];
const store = createStore(
rootReducer,
compose(...enhancers)
);
sagaMiddleware.run(rootSaga);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
//
serviceWorker.unregister();
Team.js
import React,{useEffect} from 'react'
import {useSelector,useDispatch} from "react-redux";
import {getTeams} from "../actions";
const Team=()=> {
const data=useSelector(state=>state.teams);
const dispatch=useDispatch();
useEffect(() => {
console.log("Called");
dispatch(getTeams());
}, [dispatch])
console.log("hello",data);
return (
<div>
</div>
)
}
export default Team
The problem is probably caused by axios.get() as the return is a promise. Try the following:
yield axios.get(url).then((resp) => {
return resp.data;
}).then(response => {
remoteData = response;
});
I think you can't simply call axios function directly that way. You have to wrap it in call a saga-effects which only takes a function as its argument not a Promise resolve, so it would look like:
import { call } from 'redux-saga/effects';
// Wrap in a call which takes a function as argument
const remoteData = yield call(() => axios.get(url).then(response => repsonse.data));
// Or you can simply write in shorter way
const { data: remoteData } = yield call(axios.get, url);
I am currently setting up my RTK (Redux Toolkit) and did some minor testings. Here's my code:
store/index.js
import { configureStore } from '#reduxjs/toolkit'
import { loginSliceReducer } from './views/page/login/loginSlice'
export default configureStore({
reducer: {
login: loginSliceReducer
}
})
loginSlice.js
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit'
import ApiService from '../../services/ApiService'
export const authorize = createAsyncThunk(
'api/authorize',
async (email, password) => {
const response = await ApiService.post(email, password)
return response.data
}
)
export const loginSlice = createSlice({
name: 'login',
initialState: {
loading: true,
token: null,
data: []
},
reducers: {
updateState: (state, action) => {
const { payload } = action
switch (payload.type) {
case AUTH_SUCCESS:
state.loading = false
state.token = payload.token
state.data = payload.data
break
default:
}
}
},
extraReducers: {
[authorize.fulfilled]: (state, action) => {
// ... do state update here
}
}
})
export default loginSlice.reducer
login.js
import React, { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { authorize } from './loginSlice'
const Login = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(authorize('testuser#example.com', 'test123'))
}, [])
return <div>Auth Test</div>
}
The code above doesn't work. I keep getting this error:
Error: Actions must be plain objects. Use custom middleware for async actions.
On this line:
> 25 | dispatch(authorize('testuser#example.com', 'test123'))
Please don't mind me triggering the authorize on useEffect, as this is only a test to check if the endpoint is being called and to check if the state will update once the request is successful. :-D
I had this same issue and it was caused by the fact that I was adding additional middleware.
#reduxjs/toolkit's configureStore has some middleware that is included by default, but if you add anything to the middleware property, it will overwrite these defaults.
The solution is to include the default middleware along with the middleware you define:
import { configureStore, getDefaultMiddleware } from '#reduxjs/toolkit';
import { loginSliceReducer } from './views/page/login/loginSlice';
import { otherMiddleware } from './middlewares/some-other-module';
export default configureStore({
reducer: {
login: loginSliceReducer
},
middleware: [ // Because we define the middleware property here, we need to explictly add the defaults back in.
...getDefaultMiddleware(),
otherMiddleware
]
})
Note, there is no need to explicitly include redux-thunk when using #reduxjs/toolkit because it is already part of the default middlewares from getDefaultMiddleware()
Looks like the problem is that you didn't add to your store a middleware capable of handling async actions
In your store/index.js try smth like:
import { applyMiddleware } from 'redux';
import { configureStore, getDefaultMiddleware } from '#reduxjs/toolkit'
import { loginSliceReducer } from './views/page/login/loginSlice'
import thunk from 'redux-thunk';
export default configureStore({
reducer: {
login: loginSliceReducer
},
middleware: [applyMiddleware(thunk), getDefaultMiddleware()]
})
If there are more than one argument to be passed to an action creator, you must pass them inside an object.
For example, if I have to send email and password as payload, I will have to send them in an object like below:
dispatch(authorize({email, password}))
one way you can fix this is to use your store's dispatch method.
This way, since thunk is included in Redux Toolkit (RTK) by default, you'll have access to that - which should fix the error you're getting.
Try This:
store/index.js
import { configureStore } from '#reduxjs/toolkit'
import { loginSliceReducer } from './views/page/login/loginSlice'
export default const store = configureStore({
reducer: {
login: loginSliceReducer
}
})
const useAppDispatch = () => store.dispatch
export { useAppDispatch }
login.js
import React, { useEffect } from 'react'
import { useSelector} from 'react-redux'
import { authorize } from './loginSlice'
import { useAppDispatch } from './store/index'
const Login = () => {
const dispatch = useAppDispatch()
useEffect(() => {
dispatch(authorize('testuser#example.com', 'test123'))
}, [])
return <div>Auth Test</div>
}
Sory for my English!
I am doing test work on React.js. The task is to make a regular blog. I ran into an unwanted problem. As a rule, componentDidMount makes entries, ready data and is called once.
I invoke the loadPosts action in the CDM to get the data.
The takeEvery effect sees the necessary saga, but does not cause it, but skips it.
When I press a button, everything works fine.
I'm new to React. All i tried is google
repository with project
branch - dev-fullapp
index.js
import store from "./redux/store";
const app = (
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
);
store.js
import { createStore, compose, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import apiSaga from "./sagas/index";
import rootReducer from "./reducers/index";
const initialiseSagaMiddleware = createSagaMiddleware();
const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
rootReducer,
storeEnhancers(applyMiddleware(initialiseSagaMiddleware))
);
initialiseSagaMiddleware.run(apiSaga);
export default store;
sagas.js
import { put, call, takeEvery } from "redux-saga/effects";
import { fetchGetPosts } from "../apis/index";
import { setErrorPosts, setPosts } from "../actions/actions-posts";
function* workerGetPosts() {
try {
const posts = yield call(fetchGetPosts);
yield put(setPosts(posts));
} catch (err) {
yield put(setErrorPosts(err));
}
}
export default function* watchSaga() {
yield takeEvery(POSTS.LOADING, workerGetPosts);
}
actions.js
import { POSTS } from "../constants";
export const loadPosts = () => {
console.log('action-load')
return {
type: POSTS.LOADING
}
};
export const setPosts = payload => ({
type: POSTS.LOAD_SUCCESS,
payload
});
export const setErrorPosts = error => ({
type: POSTS.ERROR,
error
});
rootReducer.js
import { combineReducers } from "redux";
import postsReducer from "./reducer-posts";
import loadReducer from "./reducer-load";
const rootReducer = combineReducers({
posts: postsReducer,
isLoad: loadReducer
});
export default rootReducer;
reducer-posts.js
import { POSTS } from "../constants";
const postState = {
posts: []
};
function postsReducer(state = postState, action) {
switch (action.type) {
case POSTS.LOAD_SUCCESS:
return {
...state,
posts: [...action.payload]
};
default:
return state;
}
}
export default postsReducer;
reducer-load.js
import { POSTS } from "../constants";
import { combineReducers } from "redux";
const loadReducerPosts = (state = false, action) => {
switch (action.type) {
case POSTS.LOADING: return false;
case POSTS.LOAD_SUCCESS: return true;
case POSTS.ERROR: return false;
default: return state;
}
};
const loadReducer = combineReducers({
isLoadPost: loadReducerPosts,
});
export default loadReducer;
news.jsx
class News extends Component {
componentDidMount() {
loadPosts();
}
render() {
CONST { loadPosts }this.props
return (
<main>
// code
<button onClick={loadPosts}>Test Button</button>
</main>
);
}
}
const mapStateToProps = (
{ posts: { loading, posts, success } }
) => ({
posts,
loading,
success
});
export default connect(
mapStateToProps,
{ loadPosts }
)(News);
loadPosts method is available as props to the React component in current case. Unlike in componentDidMount, on button click you are calling the function from props. You have to use this.props.loadPosts() on both places
I am new to the saga world. Although I have worked with thunk on react-native territory, I am very confused at the moment. I am trying to get the skeleton of my project going which I expect to get very large soon. With that in mind, I am trying to separate the logic into multiple files.
I have gotten the reducer to fire except it is not the way I want. I am not sure how it is even happening. My saga does not fire but my state updates. I see the console log from my reducer but nothing from the saga watcher function. What should I change?
Index.js
import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { Provider } from 'react-redux'
import reducer from './reducers'
import rootSaga from './sagas'
import App from './App'
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("etlRootDiv"),
);
App.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import FileExtensionSelector from './components/FileExtensionSelector'
import { setFileExtension } from './actions'
class App extends Component {
constructor(props) {
super(props)
}
handleTypeSelect() {
console.log('handle more click');
this.props.setFileExtension('zip');
console.log(this.props);
}
componentWillReceiveProps(nextProps){
console.log(nextProps);
}
render() {
return (
<div>
<FileExtensionSelector onFileTypeSelect={this.handleTypeSelect.bind(this)} />
<div>{this.props.fileType} ...asdasd</div>
</div>
)
}
}
const mapStateToProps = ({ metaState }) => {
const { fileType } = metaState;
return { fileType };
};
const mapDispatchToProps = (dispatch) => ({
setFileExtension(ext) {
dispatch(setFileExtension(ext))
}
})
export default connect(mapStateToProps, mapDispatchToProps)(App)
reducers/index.js
import { combineReducers } from 'redux';
import metaState from './MetaStateReducer';
const rootReducer = combineReducers({
metaState,
})
export default rootReducer
reducers/metastatereducer.js
const INITIAL_STATE = {
fileType: null,
hasHeader: false,
};
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
case 'SET_FILE_EXTENSION':
console.log('/// in set file reducer ///');
console.log(action);
// console.log({ ...state, ...INITIAL_STATE, fileType: action.payload });
return { ...state,...INITIAL_STATE, fileType: action.payload };
default:
return state;
}
}
actions/metaStateActions.js
function action(type, payload = {}) {
return { type, ...payload }
}
export const SET_FILE_EXTENSION = "SET_FILE_EXTENSION";
export const setFileExtension = (extension) => action( SET_FILE_EXTENSION, { payload: extension });
actions/index.js
export { setFileExtension, SET_FILE_EXTENSION } from './metaDataActions';
sagas/metastatesagas.js
import { take, put } from 'redux-saga/effects'
import { SET_FILE_EXTENSION } from '../actions';
function* watchFileExtension(ext) {
console.log(' --- in watch file ext ---');
const { extension } = yield take(SET_FILE_EXTENSION)
console.log(`set extension is ${extension}`);
// yield put({ type: 'SET_FILE_EXTENSION', payload: ext });
}
export const metaStateSagas = [
take("SET_FILE_EXTENSION", watchFileExtension),
]
sagas/index
import { all } from 'redux-saga/effects'
import { metaStateSagas } from './MetaStateSagas';
export default function* rootSaga() {
yield all([
...metaStateSagas,
])
}
redux-saga always passes an action along to the store before attempting to process itself. So, the reducers will always run before any saga behavior executes.
I think the error is that your metaStateSagas array needs to use takeEvery, not take, but I'm not entirely sure. Try that and see if it fixes things.
so i'm trying to learn more about Redux through React-Native.
i'm trying to issue a HTTPS request with Axios to pull data from a web-server. everything runs fine, i console log the action and the payload is the data i need. but it still throws a 'Cannot read property 'data' of null' TypeError.
the following is my code:
import React, { Component } from 'react';
import ReduxThunk from 'redux-thunk';
import { StackNavigator } from 'react-navigation';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import reducers from '../reducers';
import ReduxTestObj from '../components/ReduxTestObj';
export default class ReduxTest extends Component {
render() {
return (
<Provider store={createStore(reducers, {}, applyMiddleware(ReduxThunk))}>
<ReduxTestObj />
</Provider>
);
}
}
Here is the ReduxTestObj
import React, { Component } from 'react';
import _ from 'lodash';
import { View } from 'react-native';
import { connect } from 'react-redux';
import DevBanner from './DevBanner';
import Header from './Header';
import { actionCreator } from '../actions';
class ReduxTestObj extends Component {
componentWillMount() {
this.props.actionCreator('urlIWantToGoTo');
}
getBannerText() {
if (this.props.loading) {
return 'PULLING DATA. GIVE ME A SECOND';
}
const rObj = _.map(this.state.data[1].recipient_list);
const rName = [];
for (let i = 0; i < rObj.length; i++) {
rName[i] = rObj[i].team_key.substring(3);
}
const winnerMSG = `Teams ${rName[0]}, ${rName[1]}, ${rName[2]}, ${rName[3]} Won`;
return winnerMSG;
}
render() {
return (
<View>
<Header
text={'Redux Testing'}
/>
<DevBanner
message={this.getBannerText()}
/>
</View>
);
}
}
const mapStateToProps = state => {
return {
data: state.tba.data,
loading: state.tba.loading
};
};
export default connect(mapStateToProps, { actionCreator })(ReduxTestObj);
Here is the Action Creator
import axios from 'axios';
import { TBA_PULL } from './Types';
export const actionCreator = (url) => {
return (dispatch) => {
axios({
method: 'get',
url,
responseType: 'json',
headers: {
'Auth-Key':
'Hidden For Obvious Reasons'
},
baseURL: 'theBaseUrlOfTheWebsite'
}).then(response => {
dispatch({ type: TBA_PULL, payload: response.data });
});
};
};
And Here is the Reducer
import { TBA_PULL } from '../actions/Types';
const INITIAL_STATE = { data: [], loading: true };
export default (state = INITIAL_STATE, action) => {
console.log(action);
switch (action.type) {
case TBA_PULL:
return { ...state, loading: false, data: action.payload };
default:
return state;
}
};
As stated above, the console.log(action) prints out and the data it has is correct. yet it continues to throw the following error:
Error1
I've tried changing things around, googling, searching, gutting out the Action Reducer to make it as basic as possible by just returning a string. and it refuses to work.
Has anyone run into an issue similar to this or know how to fix it?
Thank you for your time.
EDIT: i also console logged 'this' as the first line of getBannerText() in ReduxTestObj and it returned successfully this.props.data as the data i want and this.props.loading as false. Yet it still throws the same error.
Welp. I guess i was just making a mistake.
i was calling This.state.data instead of this.props.data in getBannerText().
that solved the issue.