React Native Ignite error : uncaught at root at takeLatest at saga - javascript

i am new in react native with ignite boilerplate. I am try to create simple function for login checking user data with API to Express. However, i am getting error: uncaught at root at takeLatest at login ReferenceError: call is not defined.
This is my code:
Api.js
// a library to wrap and simplify api calls
import apisauce from 'apisauce'
// our "constructor"
const create = (baseURL = 'http://localhost:4200/') => {
// ------
// STEP 1
// ------
//
// Create and configure an apisauce-based api object.
//
const api = apisauce.create({
// base URL is read from the "constructor"
baseURL,
// here are some default headers
headers: {
'Cache-Control': 'no-cache'
},
// 10 second timeout...
timeout: 10000
})
// ------
// STEP 2
// ------
//
// Define some functions that call the api. The goal is to provide
// a thin wrapper of the api layer providing nicer feeling functions
// rather than "get", "post" and friends.
//
// I generally don't like wrapping the output at this level because
// sometimes specific actions need to be take on `403` or `401`, etc.
//
// Since we can't hide from that, we embrace it by getting out of the
// way at this level.
//
const getRoot = () => api.get('')
const getRate = () => api.get('rate_limit')
const getUser = (username) => api.get('search/users', {q: username})
//Method for Backend-Server
//Users Methods
const getUsers = () => api.get('/users')
const login = (username, password) => api.post('users/login', {username: username, password: password})
// ------
// STEP 3
// ------
//
// Return back a collection of functions that we would consider our
// interface. Most of the time it'll be just the list of all the
// methods in step 2.
//
// Notice we're not returning back the `api` created in step 1? That's
// because it is scoped privately. This is one way to create truly
// private scoped goodies in JavaScript.
//
return {
// a list of the API functions from step 2
getRoot,
getRate,
getUser,
getUsers,
login
}
}
// let's return back our create method as the default.
export default {
create
}
LoginRedux.js:
import { createReducer, createActions } from 'reduxsauce'
import Immutable from 'seamless-immutable'
/* ------------- Types and Action Creators ------------- */
const { Types, Creators } = createActions({
loginRequest: ['username', 'password'],
loginSuccess: ['username'],
loginFailure: ['error'],
logout: null
})
export const LoginTypes = Types
export default Creators
/* ------------- Initial State ------------- */
export const INITIAL_STATE = Immutable({
username: null,
error: null,
payload: null,
fetching: false
})
/* ------------- Reducers ------------- */
// we're attempting to login
export const request = (state) => state.merge({ fetching: true, payload: null })
// we've successfully logged in
export const success = (state, { username }) =>
state.merge({ fetching: false, error: null, username })
// we've had a problem logging in
export const failure = (state, { error }) =>
state.merge({ fetching: false, error })
// we've logged out
export const logout = (state) => INITIAL_STATE
/* ------------- Hookup Reducers To Types ------------- */
export const reducer = createReducer(INITIAL_STATE, {
[Types.LOGIN_REQUEST]: request,
[Types.LOGIN_SUCCESS]: success,
[Types.LOGIN_FAILURE]: failure,
[Types.LOGOUT]: logout
})
/* ------------- Selectors ------------- */
// Is the current user logged in?
export const isLoggedIn = (loginState) => loginState.username !== null
Redux index.js:
import { combineReducers } from 'redux'
import configureStore from './CreateStore'
import rootSaga from '../Sagas/'
export default () => {
/* ------------- Assemble The Reducers ------------- */
const rootReducer = combineReducers({
nav: require('./NavigationRedux').reducer,
github: require('./GithubRedux').reducer,
login: require('./LoginRedux').reducer,
search: require('./SearchRedux').reducer,
users: require('./UsersRedux').reducer
})
return configureStore(rootReducer, rootSaga)
}
LoginSagas.js:
import { put } from 'redux-saga/effects'
import LoginActions from '../Redux/LoginRedux'
// attempts to login
export function * login (api, { username, password }) {
const response = yield call(api.login, username, password)
if(response.ok) {
// dispatch successful logins
yield put(LoginActions.loginSuccess(username))
} else {
yield put(LoginActions.loginFailure('WRONG'))
}
// if (password === '') {
// // dispatch failure
// yield put(LoginActions.loginFailure('WRONG'))
// } else {
// // dispatch successful logins
// yield put(LoginActions.loginSuccess(username))
// }
}
index.js in Saga:
import { takeLatest, all } from 'redux-saga/effects'
import API from '../Services/Api'
import FixtureAPI from '../Services/FixtureApi'
import DebugConfig from '../Config/DebugConfig'
/* ------------- Types ------------- */
import { StartupTypes } from '../Redux/StartupRedux'
import { GithubTypes } from '../Redux/GithubRedux'
import { LoginTypes } from '../Redux/LoginRedux'
import { UsersTypes } from '../Redux/UsersRedux'
/* ------------- Sagas ------------- */
import { startup } from './StartupSagas'
import { login } from './LoginSagas'
import { getUserAvatar } from './GithubSagas'
import { getUsers } from './UsersSagas'
/* ------------- API ------------- */
// The API we use is only used from Sagas, so we create it here and pass along
// to the sagas which need it.
const api = DebugConfig.useFixtures ? FixtureAPI : API.create()
/* ------------- Connect Types To Sagas ------------- */
export default function * root () {
yield all([
// some sagas only receive an action
takeLatest(StartupTypes.STARTUP, startup),
takeLatest(LoginTypes.LOGIN_REQUEST, login, api),
// some sagas receive extra parameters in addition to an action
takeLatest(GithubTypes.USER_REQUEST, getUserAvatar, api),
takeLatest(UsersTypes.USERS_REQUEST, getUsers, api)
])
}
LoginScreen.js
import React, { PropTypes } from "react";
import { View, ScrollView, Text, TextInput, TouchableOpacity, Image, Keyboard, LayoutAnimation } from "react-native";
import { connect } from "react-redux";
import Styles from "./Styles/LoginScreenStyles";
import { Images, Metrics } from "../Themes";
import LoginActions from "../Redux/LoginRedux";
import { Button, Text as NBText, Contant, Form, Item, Input, Label } from "native-base";
import UsersActions from "../Redux/UsersRedux"
class LoginScreen extends React.Component {
static propTypes = {
dispatch: PropTypes.func,
fetching: PropTypes.bool,
attemptLogin: PropTypes.func,
getUsers: PropTypes.func
};
isAttempting = false;
keyboardDidShowListener = {};
keyboardDidHideListener = {};
constructor(props) {
super(props);
this.props.getUsers();
this.state = {
username: "reactnative#infinite.red",
password: "password",
visibleHeight: Metrics.screenHeight,
topLogo: { width: Metrics.screenWidth - 40 },
};
this.isAttempting = false;
}
componentWillReceiveProps(newProps) {
this.forceUpdate();
// Did the login attempt complete?
if (this.isAttempting && !newProps.fetching) {
this.props.navigation.goBack();
}
if(newProps.users!= null) {
console.log(newProps.users)
}
if(newProps.login != null) {
console.log("LOGIN TRY");
console.log(newProps.login);
}
if(newProps.fetching != null) {
console.log("LOGIN TRY 2");
console.log(newProps.fetching);
}
}
componentWillMount() {
// Using keyboardWillShow/Hide looks 1,000 times better, but doesn't work on Android
// TODO: Revisit this if Android begins to support - https://github.com/facebook/react-native/issues/3468
this.keyboardDidShowListener = Keyboard.addListener("keyboardDidShow", this.keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener("keyboardDidHide", this.keyboardDidHide);
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
keyboardDidShow = e => {
// Animation types easeInEaseOut/linear/spring
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
let newSize = Metrics.screenHeight - e.endCoordinates.height;
this.setState({
visibleHeight: newSize,
topLogo: { width: 100, height: 70 },
});
};
keyboardDidHide = e => {
// Animation types easeInEaseOut/linear/spring
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
this.setState({
visibleHeight: Metrics.screenHeight,
topLogo: { width: Metrics.screenWidth - 40 },
});
};
handlePressLogin = () => {
// const { username, password } = this.state
// this.isAttempting = true
// attempt a login - a saga is listening to pick it up from here.
this.props.attemptLogin(this.state.username, this.state.password);
console.log("Try to login");
console.log(this.props.login);
this.props.navigation.navigate("LaunchScreen");
};
handleChangeUsername = text => {
this.setState({ username: text });
};
handleChangePassword = text => {
this.setState({ password: text });
};
render() {
const { username, password } = this.state;
const { fetching } = this.props;
const editable = !fetching;
const textInputStyle = editable ? Styles.textInput : Styles.textInputReadonly;
return (
<ScrollView
contentContainerStyle={{ justifyContent: "center" }}
style={[Styles.container, { height: this.state.visibleHeight }]}
keyboardShouldPersistTaps="always"
>
<Image source={Images.logo} style={[Styles.topLogo, this.state.topLogo]} />
<View style={Styles.form}>
<Form>
<Item stackedLabel>
<Label>Username</Label>
<Input
ref="username"
value={username}
editable={editable}
keyboardType="default"
returnKeyType="next"
autoCapitalize="none"
autoCorrect={false}
onChangeText={this.handleChangeUsername}
underlineColorAndroid="transparent"
onSubmitEditing={() => this.password._root.focus()}
/>
</Item>
<Item stackedLabel>
<Label>Password</Label>
<Input
ref={ref => (this.password = ref)}
value={password}
editable={editable}
keyboardType="default"
returnKeyType="go"
autoCapitalize="none"
autoCorrect={false}
secureTextEntry
onChangeText={this.handleChangePassword}
underlineColorAndroid="transparent"
onSubmitEditing={this.handlePressLogin}
/>
</Item>
</Form>
<View style={[Styles.loginRow]}>
<Button style={{ flex: 1, justifyContent: "center" }} full onPress={this.handlePressLogin}>
<NBText>Sign In</NBText>
</Button>
<Button
style={{ flex: 1, justifyContent: "center" }}
full
onPress={() => this.props.navigation.goBack()}
>
<NBText>Cancel</NBText>
</Button>
</View>
</View>
</ScrollView>
);
}
}
const mapStateToProps = state => {
return {
fetching: state.login.fetching,
login: state.login,
users: state.users
};
};
const mapDispatchToProps = dispatch => {
return {
attemptLogin: (username, password) => dispatch(LoginActions.loginRequest(username, password)),
getUsers: () => dispatch(UsersActions.usersRequest())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(LoginScreen);
Please help thanks.

You need to import call to use it
as import {call, put} from 'redux-saga/effects', in your LoginSagas.js

Related

react saga - Filter data based on query's body

In my backend, I use the post request type to fetch the data, so I can send a query or any other information I want in the body, so that the data is fetched on its basis
On the front end, I use React with Saga, and I can fetch data from the server without any problem
The case in which I need help is as follows.
I have a table containing products in the database and I want to divide these products into three pages based on a field in the table
Of course, I was able to do the filtering process in Saga as follows, and there is no problem
How can I pass this query in the image variable according to my need?
Suppose I want to create three pages in React, and for each page I will send a different filter, how do I do that?
I will attach the full code that I tried to do
Waiting for your help
this action
import {
GET_SERVICES_SUPPORT_LIST,
API_RESPONSE_SUCCESS,
API_RESPONSE_ERROR,
} from "./actionType";
// common success
export const ServicesSupportApiResponseSuccess = (actionType, data) => ({
type: API_RESPONSE_SUCCESS,
payload: { actionType, data },
});
// common error
export const ServicesSupportApiResponseError = (actionType, error) => ({
type: API_RESPONSE_ERROR,
payload: { actionType, error },
});
export const getServicesSupportList = services => ({
type: GET_SERVICES_SUPPORT_LIST,
payload: services,
});
this action type
// Actions
export const API_RESPONSE_SUCCESS = "API_RESPONSE_SUCCESS";
export const API_RESPONSE_ERROR = "API_RESPONSE_ERROR";
export const GET_SERVICES_SUPPORT_LIST = "GET_SERVICES_SUPPORT_LIST";
this saga
import { call, put, takeEvery, all, fork } from "redux-saga/effects";
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import {
GET_SERVICES_SUPPORT_LIST,
} from "./actionType";
import {
ServicesSupportApiResponseSuccess,
ServicesSupportApiResponseError
} from "./action";
//Include Both Helper File with needed methods
import {
getServicesSupportList as getServicesSupportApi
}
from "../../helpers/backend_helper";
function* getServicesSupport({ payload: services }) {
try {
const response = yield call(getServicesSupportApi, services);
yield put(ServicesSupportApiResponseSuccess(GET_SERVICES_SUPPORT_LIST, response.data));
} catch (error) {
yield put(ServicesSupportApiResponseError(GET_SERVICES_SUPPORT_LIST, error));
}
}
export function* watchGetServicesSupportList() {
yield takeEvery(GET_SERVICES_SUPPORT_LIST, getServicesSupport);
}
function* servicesSupportSaga() {
yield all(
[
fork(watchGetServicesSupportList),
]
);
}
export default servicesSupportSaga;
this hook
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux'
import { getServicesSupportList} from "../../../store/actions";
const AllServicesSupportHook = () => {
const dispatch = useDispatch();
//to get state from redux
const { servicesSupportList, loder,error} = useSelector((state) => ({
servicesSupportList: state.ServicesSupport.servicesSupportList,
loder: state.ServicesSupport.loder,
error: state.ServicesSupport.error,
}));
//when first load
useEffect(() => {
if (loder) {
dispatch(getServicesSupportList());
}
}, [dispatch]);
let test ;
try {
if (servicesSupportList.paginator)
test = {query: {categoryTickets : 2} //This is definitely wrong
} catch (e) { }
const getPage = (services) => {
dispatch(getServicesSupportList(services));
}
return [servicesSupportList, loder,error,getPage]
};
export default AllServicesSupportHook;
get list
export const getServicesSupportList = (services) => api.create(url.GET_SERVICES_SUPPORT_LIST,services);
this reducer
import {
GET_SERVICES_SUPPORT_LIST,
API_RESPONSE_SUCCESS,
API_RESPONSE_ERROR,
} from "./actionType";
const INIT_STATE = {
servicesSupportList: [],
loder: true
};
const ServicesSupport = (state = INIT_STATE, action) => {
switch (action.type) {
case API_RESPONSE_SUCCESS:
switch (action.payload.actionType) {
case GET_SERVICES_SUPPORT_LIST:
return {
...state,
servicesSupportList: action.payload.data,
loder: false,
};
default:
return { ...state };
}
case API_RESPONSE_ERROR:
switch (action.payload.actionType) {
case GET_SERVICES_SUPPORT_LIST:
return {
...state,
error: action.payload.error,
loder: true,
};
default:
return { ...state };
}
case GET_SERVICES_SUPPORT_LIST: {
return {
...state,
loder: false,
};
}
default:
return { ...state };
}
};
export default ServicesSupport;
this components
import React from "react";
import { Row, Container ,Alert} from "reactstrap";
import ServicesSupportListItems from "./ServicesSupportListCard";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import Loader from "../../Common/Loader";
const ServicesSupportList = ({ data, loder, error }) => {
return (
<Container fluid={true}>
<Row>
{loder === false ? (
data.data ? (
data.data.map((item, index) => {
return (
<ServicesSupportListItems
key={index}
id={item.id}
categoryTickets={item.categoryTickets}
title={item.titleProduct}
description={item.description}
point={item.point}
/>
);
})
) : (<Alert color="danger"> {error} </Alert>)
) : ( <Loader error={error} />
)}
<ToastContainer closeButton={false} />
</Row>
</Container>
);
};
export default ServicesSupportList;
I found the solution, thank you
function* getServicesSupport({ payload: services }) {
try {
var response;
if (services === "Support") {
response = yield call(getServicesSupportApi,{query: {categoryTickets : 2}});
}
if (services === "CardFree") {
response = yield call(getServicesSupportApi,{query: {categoryTickets : 6}});
}
yield put(ServicesSupportApiResponseSuccess(GET_SERVICES_SUPPORT_LIST, response.data));
} catch (error) {
yield put(ServicesSupportApiResponseError(GET_SERVICES_SUPPORT_LIST, error));
}
}

Server Error Error: Invalid hook call. Hooks can only be called inside of the body of a function component in _app.js

I am a newbie in React and Next JS, I want to set initial auth user data on initial load from the __app.js. But using dispatch throwing error "Invalid hook call". I know according to docs calling hooks in render function is wrong. but I am looking for an alternate solution to this.
How I can set auth data one-time so that will be available for all the pages and components.
I am including my code below.
/contexts/app.js
import { useReducer, useContext, createContext } from 'react'
const AppStateContext = createContext()
const AppDispatchContext = createContext()
const reducer = (state, action) => {
switch (action.type) {
case 'SET_AUTH': {
return state = action.payload
}
default: {
throw new Error(`Unknown action: ${action.type}`)
}
}
}
export const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, {})
return (
<AppDispatchContext.Provider value={dispatch}>
<AppStateContext.Provider value={state}>
{children}
</AppStateContext.Provider>
</AppDispatchContext.Provider>
)
}
export const useAuth = () => useContext(AppStateContext)
export const useDispatchAuth = () => useContext(AppDispatchContext)
/_app.js
import 'bootstrap/dist/css/bootstrap.min.css'
import '../styles/globals.css'
import App from 'next/app'
import Layout from '../components/Layout'
import { mutate } from 'swr'
import { getUser } from '../requests/userApi'
import { AppProvider, useDispatchAuth } from '../contexts/app'
class MyApp extends App {
render() {
const dispatchAuth = useDispatchAuth()
const { Component, pageProps, props } = this.props
// Set initial user data
const setInitialUserData = async () => {
if (props.isServer) {
const initialData = {
loading: false,
loggedIn: (props.user) ? true : false,
user: props.user
}
const auth = await mutate('api-user', initialData, false)
dispatchAuth({
type: 'SET_AUTH',
payload: auth
})
}
}
//----------------------
// Set initial user data
setInitialUserData()
//----------------------
return (
<AppProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</AppProvider>
)
}
}
MyApp.getInitialProps = async (appContext) => {
let isServer = (appContext.ctx.req) ? true : false
let user = null
let userTypes = {}
// Get user server side
if (isServer) {
await getUser()
.then(response => {
let data = response.data
if (data.status == true) {
// Set user
user = data.data.user
userTypes = data.data.user_types
//---------
}
})
.catch(error => {
//
})
}
//---------------------
return {
props: {
user,
userTypes,
isServer
}
}
}
export default MyApp
I believe this is the intended use of the useEffect hook with an empty array as its second argument:
https://reactjs.org/docs/hooks-effect.html
import {useEffect} from 'react'
class MyApp extends App {
useEffect(()=> {
setInitialUserData()
},[])
render() {
...
}
}

Next JS Redux not update server store

The Redux Update operations I made on the client-side in Next JS are not updated in the server store.
Hello. I have a problem. I'm developing an SSR application with Next JS. I have provided the next js link with the next-redux-wrapper. State update operations can be provided. State updates I made on the server-side can be viewed on the client-side. The redux updates I made on the client-side also appear on the client-side, but when I refresh the page, it returns to the previous position. Sample scenario:
Users have addresses. Their addresses can be taken from the DB and printed on the screen. DB updates when I add a new address or delete the old address. Along with it, it is updated in the store on the client-side. So far there is no problem. However, when I refresh the page, for example, if there are 4 addresses before updating and I deleted one, after the refresh, it is printed as 4 addresses again. It continues like this until I get data from the server again.
How can I move the client-side store updates to the server-side without having to make requests to the server over and over again?
store.js
// store.js
import { createStore, applyMiddleware } from 'redux';
import { createWrapper } from "next-redux-wrapper";
import thunkMiddleware from 'redux-thunk'
// ROOT REDUCERS
import rootReducer from "../reducers";
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension')
return composeWithDevTools(applyMiddleware(...middleware))
}
return applyMiddleware(...middleware)
}
const store_ = (initialState) => {
return createStore(rootReducer, initialState, bindMiddleware([thunkMiddleware]));
}
const wrapper = createWrapper(store_/*, { debug: true }*/);
export {
wrapper
}
_app.js
// _app.js
const MyApp = ({props, Component, pageProps }) => {
const store = useStore();
if (!store.getState().R_PageSettings.initStore)
{
store.dispatch({
type: HYDRATE,
payload: {
...props.initialState
}
})
}
return (
<>
<Head>
<title>{ variables.meta.title }</title>
</Head>
<Component {...pageProps} />
</>
)
}
const wrappedApp = wrapper.withRedux(MyApp);
export default wrappedApp;
wrappedApp.getInitialProps = async ctx => {
const data = await wrapper.getServerSideProps(
async (req) => {
const { store, ctx } = req;
const reduxStates = store.getState();
let user = reduxStates.R_User.user;
if (!user)
{
const cookies = parseCookies(ctx);
if (cookies.usr && user !== undefined)
{
const getUser = await CustomersController.tokenLoginControl(cookies.usr);
if (getUser && getUser.status)
{
store.dispatch(setUserSSR(getUser.user))
user = getUser.user;
}
else
destroyCookie(ctx, 'usr');
}
}
return {
user
}
}
)(ctx)
return data;
}
action.js
// CONSTANTS
import {
C_User
} from "../constants";
export const setUserSSR = user => {
return {
type: C_User.SET_USER,
payload: {
user
}
}
}
export const setUser = user => dispatch => {
return dispatch({
type: C_User.SET_USER,
payload: {
user
}
})
}
addresspage.js
// addresspage.js
import { connect } from 'react-redux';
import { bindActionCreators } from "redux";
// COMPONENTS
import UserPageLayout from "../UserPagesLayout";
import {
CustomerAddressForm
} from "../../../components";
// CONTROLLERS
import {
CustomersController
} from "../../../controllers";
// ACTIONS
import {
setUser
} from "../../../actions";
const MyAddressPage = connect(({ R_User }) => {
return {
R_User
}
}, dispatch => {
return {
setUser: bindActionCreators(setUser, dispatch)
}
})((props) => {
const addAddressHandle = () => {
props.fullBarOpen(
<CustomerAddressForm confirmHandle={async (address, setLoading) => {
const execute = await CustomersController.addAddress(address);
if (execute.status)
{
await props.setUser(execute.user);
}
else
{
setLoading(false);
}
}}
/>
);
}
return (
<UserPageLayout>
</UserPageLayout>
);
})
export default MyAddressPage;

React/Redux/REST: sending an array

I'm working on a MERN project and I need your support. Which is the best way to get data with a REST call providing an array?
I have an array of ID's and would like to take data using axios and the mongoose method "findById".
I'm sending the array as params from the component; dispatch the action fetchItinerariesId that call the function fetchItinerariesId(favItin_id) where favItin_id is the array of id's, and the reducer add the payload to my state favItineraries: [].
Here below the code:
REST api call:
const express = require("express");
const router = express.Router();
const itineraryModel = require("../../models/itineraryModel");
//! GET FAV ITINERARIES //----------------------------------------------
//* #route GET api/itineraries/:itin_id
//* #desc Get itineraries per fav itin id
//* #access Public
router.get("/itineraries/:favItin_id", (req, res) => {
let itineraryRequestedId = req.params.favItin_id;
itineraryModel
.findById(itineraryRequestedId)
.then(itinerary => {
res.send(itinerary);
})
.catch(err => console.log(err));
});
module.exports = router;
Redux Actions:
import {
FETCH_ITINERARIES_ID_PENDING,
FETCH_ITINERARIES_ID_SUCCESS
} from "./typesActions";
//! FETCH FAVORITES BY USER ID //-------------------------------------------------------------
export function fetchItinerariesIdPending() {
return {
type: FETCH_ITINERARIES_ID_PENDING
};
}
export const fetchItinerariesId = favItin_id => dispatch => {
console.log("inside action fetchItineraries per fav ID");
console.log(favItin_id);
dispatch(fetchItinerariesIdPending());
axios
.get("/api/profile/itineraries", { favItin_id: favItin_id })
.then(res => {
dispatch({
type: FETCH_ITINERARIES_ID_SUCCESS,
payload: res.data
});
});
};
Redux Reducer
FETCH_ITINERARIES_ID_PENDING,
FETCH_ITINERARIES_ID_SUCCESS
} from "../actions/typesActions";
const initialState = {
pending: false,
favItineraries: []
};
function profileReducer(state = initialState, action) {
switch (action.type) {
// GETS FAVORITES FROM ITINERARIES USING ITIN ID
case FETCH_ITINERARIES_ID_PENDING:
return {
...state,
pending: true
};
case FETCH_ITINERARIES_ID_SUCCESS:
return {
...state,
pending: false,
favItineraries: action.payload
};
default:
return state;
}
}
export default profileReducer;
React view component:
/*----- MATERIAL UI -----*/
import Box from "#material-ui/core/Box";...
/*----- REACT/ROUTER/REDUX -----*/
import React, { Component, Fragment } from "react";
import { connect } from "react-redux"; // connect component to redux store.
/*----- COMPONENTS/ACTIONS -----*/
import Navbar from "../components/Navbar";
import ItininerariesList from "../components/ItinerariesList";
import { loadUser } from "../store/actions/authActions";
import { fetchItinerariesId } from "../store/actions/profileActions";
import { fetchActivities } from "../store/actions/activitiesActions";
class Profile extends Component {
componentDidMount() {
this.props.loadUser();
let favItin_id = this.props.favItin_id;
this.props.fetchItinerariesId(favItin_id);
}
render() {
const { favitineraries, activities } = this.props;
return (
<Fragment>
<Container maxWidth="sm">
<Typography component="div">
<Box fontSize="h7.fontSize" textAlign="left" mb="3">
Favorites MYtineraries:
</Box>
<ItininerariesList
itineraries={favitineraries}
activities={activities}
/>
</Typography>
</Container>
<div className="navbar">
<Navbar />
</div>
</Fragment>
);
}
}
const mapStateToProps = (state, ownProps) => {
return {
favItin_id: state.auth.user.favorites,
favitineraries: state.profileRed.favItineraries,
activities: state.activitiesRed.activities
};
};
export default connect(mapStateToProps, {
loadUser,
fetchItinerariesId,
fetchActivities
})(Profile);
Any suggestions or guidance would be greatly appreciated. Thank you in advance.

Redux-Saga is running twice for a single action

I have a saga to handle like requests. The user clicks a button to toggle the liked status of a photo.
The saga listens for an action of type SUBMIT_LIKE. My problem is that the submitLikeSaga is running twice for each SUBMIT_LIKE action.
e.g. In the api-error case one SUBMIT_LIKE action triggers two api calls and four RECEIVE_LIKE actions.
(using react-boilerplate if that helps.)
export function* submitLikeSaga(action) {
// optimistically update the UI
// action shape: {
// type: RECEIVE_LIKE,
// like: {id: 1, liked: true}
// }
yield put(receiveLike(action.like));
// POST like data to api
const response = yield call(
request,
`${API_ENDPOINT}/user_likes.json`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${action.token}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(action.like),
}
);
// if api call failed, reverse change made to UI
if (response.err) {
yield put(receiveLike({
id: action.like.id,
liked: !action.like.liked,
}));
}
}
export function* watchSubmitLike() {
yield* takeEvery(SUBMIT_LIKE, submitLikeSaga);
}
// All sagas to be loaded
export default [
watchFetchUsers,
watchSubmitLike,
];
EDIT: Add middleware and view code.
ProfileGrid.js
const ProfileGrid = ({
users,
submitLike,
token,
}) =>
<div className={styles.profileGrid}>
{users.map((user, i) => (
<div key={i} className={styles.gridTile}>
<GridTile
title={user.username}
actionIcon={<ActionIcon
onIconClick={() => { submitLike(user.id, !user.liked, token); }}
isActive={user.liked}
activeColor="yellow"
defaultColor="white"
/>}
>
<img style={{ width: '100%', height: 'auto' }} src={user.avatar} alt="profile" />
</GridTile>
</div>
))}
</div>;
ActionIcon.js
const ActionIcon = ({
onIconClick,
isActive,
activeColor,
defaultColor,
}) =>
<IconButton onClick={onIconClick} >
<StarBorder
color={isActive ? activeColor : defaultColor}
/>
</IconButton>;
store.js
/**
* Create the store with asynchronously loaded reducers
*/
import { createStore, applyMiddleware, compose } from 'redux';
import { fromJS } from 'immutable';
import { routerMiddleware } from 'react-router-redux';
import createSagaMiddleware from 'redux-saga';
import createReducer from './reducers';
const sagaMiddleware = createSagaMiddleware();
const devtools = window.devToolsExtension || (() => (noop) => noop);
export default function configureStore(initialState = {}, history) {
// Create the store with two middlewares
// 1. sagaMiddleware: Makes redux-sagas work
// 2. routerMiddleware: Syncs the location/URL path to the state
const middlewares = [
sagaMiddleware,
routerMiddleware(history),
];
const enhancers = [
applyMiddleware(...middlewares),
devtools(),
];
const store = createStore(
createReducer(),
fromJS(initialState),
compose(...enhancers)
);
// Extensions
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {}; // Async reducer registry
// Make reducers hot reloadable, see http://mxs.is/googmo
/* istanbul ignore next */
if (module.hot) {
module.hot.accept('./reducers', () => {
System.import('./reducers').then((reducerModule) => {
const createReducers = reducerModule.default;
const nextReducers = createReducers(store.asyncReducers);
store.replaceReducer(nextReducers);
});
});
}
return store;
}
asyncInjectors.js
import { conformsTo, isEmpty, isFunction, isObject, isString } from 'lodash';
import invariant from 'invariant';
import warning from 'warning';
import createReducer from '../reducers';
/**
* Validate the shape of redux store
*/
export function checkStore(store) {
const shape = {
dispatch: isFunction,
subscribe: isFunction,
getState: isFunction,
replaceReducer: isFunction,
runSaga: isFunction,
asyncReducers: isObject,
};
invariant(
conformsTo(store, shape),
'(app/utils...) asyncInjectors: Expected a valid redux store'
);
}
/**
* Inject an asynchronously loaded reducer
*/
export function injectAsyncReducer(store, isValid) {
return function injectReducer(name, asyncReducer) {
if (!isValid) checkStore(store);
invariant(
isString(name) && !isEmpty(name) && isFunction(asyncReducer),
'(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function'
);
store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign
store.replaceReducer(createReducer(store.asyncReducers));
};
}
/**
* Inject an asynchronously loaded saga
*/
export function injectAsyncSagas(store, isValid) {
return function injectSagas(sagas) {
if (!isValid) checkStore(store);
invariant(
Array.isArray(sagas),
'(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions'
);
warning(
!isEmpty(sagas),
'(app/utils...) injectAsyncSagas: Received an empty `sagas` array'
);
sagas.map(store.runSaga);
};
}
/**
* Helper for creating injectors
*/
export function getAsyncInjectors(store) {
checkStore(store);
return {
injectReducer: injectAsyncReducer(store, true),
injectSagas: injectAsyncSagas(store, true),
};
}
According to documentation : https://redux-saga.js.org/docs/api/index.html#takelatestpattern-saga-args this will take your latest call and and will fire only one action
import { fork, takeLatest } from 'redux-saga/effects';
export function* watchSubmitLike() {
yield fork(takeLatest, SUBMIT_LIKE, submitLikeSaga);
}
Your app will work fine if you make following
yield* takeEvery(SUBMIT_LIKE, submitLikeSaga); should be
yield takeEvery(SUBMIT_LIKE, submitLikeSaga);
You should use yield* only when you are sequencing sagas
I had this happen recently. In my case the saga was getting created twice. We we withSaga HOC (generally in a container)

Categories