So i am working with redux and i wrote a reducer to manage todos;
import { combineReducers } from "redux";
import { ADD_TODO, COMPELETE_TODO, REMOVE_TODO } from "./actiontypes";
const initialState = {
todos: [],
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload],
};
case COMPELETE_TODO:
case REMOVE_TODO:
return {
...state,
todos: state.todos.filter(
(todo) => state.todos.indexOf(todo) != action.payload
),
};
default:
return state;
}
};
export default combineReducers({
rootReducer,
});
As you can see that I created a reducer and user combineReducer and then passed it to store in other file and it is working fine.
Now how much I understood combineReducer is that it combines seperated reducers when we write them seperately. So if i change the above reducer (as i have only single reducer) to following;
import { ADD_TODO, COMPELETE_TODO, REMOVE_TODO } from "./actiontypes";
const initialState = {
todos: [],
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload],
};
case COMPELETE_TODO:
case REMOVE_TODO:
return {
...state,
todos: state.todos.filter(
(todo) => state.todos.indexOf(todo) != action.payload
),
};
default:
return state;
}
};
export default rootReducer;
And pass this to the state but it gives error. It gives undefined when i try to access todos by using useSelector;
const {todos} = useSelector(state => ({
todos: state.rootReducer.todos
}))
So what i think is i didn't understood combineReducers properly. Please explain what is causing the error above.
Combine reducer adds namespaces to the state controlled by each reducer based on the keys of the object you pass in. When you don't use combine reducer, there won't be a namespace. So change state.rootReducer to state.
combineReducers creates a reducer that separates the state of the combined reducers. In the documentation they show this example:
rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer})
which would produce the following state object
{
potato: {
// ... potatoes, and other state managed by the potatoReducer ...
},
tomato: {
// ... tomatoes, and other state managed by the tomatoReducer, maybe some nice sauce? ...
}
}
Thus when you write
combineReducers({
rootReducer,
});
The state object will be
{
rootReducer: {
// rootReducer's state
}
}
In your second example you just return the rootReducer. Therefore there are no separate states. The rootReducer operates on the root state. Thus you must adjust the selection.
const {todos} = useSelector(state => ({
todos: state.todos
}))
Related
I am unable to dispatch an action to reducer.
State comes out just fine. Not sure where I have gone wrong.
For other user reducer, dispatch works just fine. my project has nested drawer navigation > tabs navigation > stack navigation and current page is the 3rd stack screen. Not sure if that is the issue or what.
Store.js
import { combineReducers, createStore } from '#reduxjs/toolkit';
import cartReducer from './cartSlice';
import userReducer from './userSlice';
const rootReducer = combineReducers({
userReducer: userReducer,
cartReducer: cartReducer,
})
const configureStore = () => createStore(rootReducer)
export default configureStore
cartSlice.js
const initialState = {
cart: [{ key: 1, data: { freq: 'Daily', duration: 30 } }]
}
const cartReducer = (state = initialState, action) => {
switch (action.types) {
case "ADD_TO_CART":
console.log(action)
return {
...state,
cart: [...state.cart, { key: 2, data: action.data }]
}
case "REMOVE_FROM_CART":
const idx = state.cart.map((cartItem) => (
cartItem.key === action.id
))
const tempNewCart = [...state.cart]
if (idx >= 0) {
tempNewCart.splice(idx, 1)
}
return { ...state, cart: tempNewCart }
case "CLEAR_CART":
return {
...state,
cart: []
}
default:
return state
}
}
export default cartReducer
SubscriptionQuantity Component
const mapStateToProps = (state) => {
console.log(state)
return {
cart: state.cartReducer.cart
}
}
const mapDispatchToProps = (dispatch) => {
// console.log(dispatch)
return {
addtoCartStore: (freq, duration) => dispatch({
type: 'ADD_TO_CART',
data: {
freq: freq,
duration: duration
}
})
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SubscriptionQuantity)
function call for dispatch
const addtoCart = () => {
addtoCartStore('Alternate', 30)
navigation.navigate("CartScreen")
}
The issue was with the switch statement!!!!
it should be switch(action.type) not action.types. that's why it was going in on the default route. changed the typo and works fine now.!
how can I get the value of the state from the store?
I already tried using store.getState().isLoggedIn but it didn't work:
import * as actions from 'actions/actions'
import store from 'store/store'
store.dispatch(actions.login(this.state.username,this.state.password)).then(() => {
state = store.getState();
alert(state.isLoggedIn);
});
I'm trying to get the value of the state "isLoggedIn" but it returns "undefined". how can i properly get the state value from the store?
here is my source code
Reducer:
const INITIAL_STATE={
isLoggedIn:false,
isLoading:false,
userData:{},
error:undefined
}
export default function auth(state=INITIAL_STATE,action){
console.log(action.type);
switch (action.type) {
case 'LOGIN_ATTEMPT':
return{
...state,
isLoading:true,
isLoggedIn:false
}
break;
case 'LOGIN_SUCCESS':
return {
...state,
isLoading:false,
isLoggedIn:true,
userData:action.userData,
error:undefined
}
break;
case 'LOGIN_FAILED':
return{
...state,
isLoading:false,
isLoggedIn:false,
error:action.error
}
break;
case 'LOGOUT':
return{
...state,
isLoading:false,
isLoggedIn:false
}
break;
default:
return state
}
}
actions:
export function isLoading(bool:Boolean){
return{
type:'LOGIN_ATTEMPT',
isLoading:bool
}
}
export function loginSuccess(userData:Object){
return{
type:'LOGIN_SUCCESS',
userData
}
}
export function loginFailed(error:Object){
return{
type:'LOGIN_FAILED',
error
}
}
export function login(uname,pass){
return dispatch => {
dispatch(isLoading(true));
return fetch('http://192.168.99.1:5000/user/login',{
method:'POST',
headers:{
'Content-Type':'application/json'
},
body:JSON.stringify({
username:uname
})
})
.then((response) => {
if(response.status < 300){
dispatch(isLoading(false))
response.json().then((responseJSON) => {
if(responseJSON['message'] == "True" && bcrypt.compareSync(pass, responseJSON['hash']) ){
console.log("responseJSON",responseJSON);
dispatch(loginSuccess(responseJSON));
}
else{
dispatch(isLoading(false))
dispatch(loginFailed(responseJSON.message))
}
})
}
else{
response.json().then((responseJSON) => {
console.log("responseJSON",responseJSON);
dispatch(isLoading(false))
dispatch(loginFailed(responseJSON.message))
})
}
})
.catch((error) => {
console.log("error",error);
dispatch(isLoading(false))
dispatch(loginFailed(error))
})
}
}
store:
import {combineReducers} from 'redux'
import auth from './authReducer'
import {createStore,applyMiddleware} from 'redux'
import thunk from 'redux-thunk';
const rootReducer = combineReducers({
auth
})
let store = createStore(rootReducer,applyMiddleware(thunk))
export default store;
I was able to view the data by using JSON.stringify
JSON.stringify(store.getState())
it returns json data. using this method makes it easy to view objects
You're using combineReducers to produce rootReducer.
The state produced by combineReducers() namespaces the states of each reducer under their keys as passed to combineReducers()
So store.getState() will return object like this
{ auth:
{ isLoggedIn:false,
isLoading:false,
userData:{},
error:undefined
}
}
To refer to isLoggedIn you have to do alert(state.auth.isLoggedIn);
This is the filterAction action that emits a value to to the persisted reducer. This action is called with dispatch() in a dropdown in a component.
import { SORT_BY_TITLE, SORT_BY_RELEASED_AT } from './types';
// sort by title
export const sortByTitle = () => ({
type: SORT_BY_TITLE
});
// sort by released at
export const sortByReleasedAt = () => ({
type: SORT_BY_RELEASED_AT
});
The corresponding filterReducer
import { SORT_BY_TITLE, SORT_BY_RELEASED_AT } from '../actions/types';
const initialState = {
sortBy: 'title'
};
export default function(state = initialState, action) {
switch(action.type) {
case SORT_BY_TITLE:
return {
...state,
sortBy: 'title'
};
case SORT_BY_RELEASED_AT:
return {
...state,
sortBy: 'releasedAt'
};
default:
return state;
}
};
The filterReducer value is the one persisted in the main combined reducer.
export default combineReducers({
books: booksReducer,
book: bookReducer,
form: addBookFormReducer,
filter: filterReducer
});
The app's store
import { createStore, applyMiddleware } from 'redux';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
const middleware = [thunk];
const persistConfig = {
key: 'root',
storage,
whitelist: ['filter']
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = createStore(persistedReducer, {}, applyMiddleware(...middleware));
export const persistor = persistStore(store);
The value persisted on the filter part of the main reducer gets displayed in the dropdown changes on which call the filterAction with dispatch().
Here's the booksReducer that creates the books part of the app's store so books are displayed in a component.
import {
GET_BOOKS,
BOOKS_LOADING,
DELETE_BOOK,
SORT_BY_TITLE,
SORT_BY_RELEASED_AT
} from '../actions/types';
const initialState = {
books: [],
loading: false
};
const booksReducer = (state = initialState, action) => {
switch(action.type) {
case BOOKS_LOADING:
return {
...state,
loading: true
};
case GET_BOOKS:
return {
...state,
books: action.payload,
loading: false
};
case DELETE_BOOK:
return {
books: [...state.books.filter(book => book._id !== action.payload.id)]
};
case SORT_BY_TITLE:
return {
...state,
books: [...state.books.sort((a, b) => a.title < b.title ? -1 : 1 )]
};
case SORT_BY_RELEASED_AT:
return {
...state,
books: [...state.books.sort((a, b) => a.releasedAt < b.releasedAt ? -1 : 1 )]
};
default:
return state;
}
};
export default booksReducer;
The filter part of the main reducer persists Ok on the page reload however the books list is displayed with the default by title sort.
How do I get the app to persist the sort on the page reload? The complete repo is on https://github.com/ElAnonimo/booklister
On the BookList load the getBooks() action return reset the sorted books list to its default sorting so it was easier to persist only the filter prop of the store then sort the books list on each load of the BookList component.
The changes were made
to booksReducer
import {
GET_BOOKS,
BOOKS_LOADING,
DELETE_BOOK
} from '../actions/types';
const initialState = {
books: [],
loading: false
};
const booksReducer = (state = initialState, action) => {
switch(action.type) {
case BOOKS_LOADING:
return {
...state,
loading: true
};
case GET_BOOKS:
return {
...state,
books: action.payload,
loading: false
};
case DELETE_BOOK:
return {
books: [...state.books.filter(book => book._id !== action.payload.id)]
};
default:
return state;
}
};
export default booksReducer;
to BookList
class BookList extends Component {
componentDidMount() {
this.props.getBooks();
}
applySorting(books) {
const sortBy = this.props.filter.sortBy;
if (!sortBy) {
return books;
}
return books.sort((a, b) => a[sortBy] < b[sortBy] ? -1 : 1);
}
render() {
const { books, loading } = this.props.books;
let booksContent;
if (!books || loading) {
booksContent = <Spinner />;
} else {
if (books.length > 0) {
booksContent = this.applySorting(books).map(book => <BookItem book={book} key={book._id} />);
} else {
booksContent = <h4>No books found</h4>;
}
}
...
...
I have two reducers which share some actions. Issue is when i dispatch action on one screen it triggers action on the other screen. Boths screen are in a TabNavigator therefore I can easily see that if I change a field on 1 screen the same field is changed on the other screen as well.
Reducer 1
import * as Actions from '../actions/Types';
const initialState = {
email: '',
password: ''
};
const signInReducer = (state = initialState, action) => {
switch(action.type) {
case Actions.CHANGE_EMAIL_INPUT:
return Object.assign({}, state,
{ email: action.email }
);
case Actions.CHANGE_PASSWORD_INPUT:
return Object.assign({}, state,
{ password: action.password }
);
default:
return state;
}
}
export default signInReducer;
Reducer 2
import * as Actions from '../actions/Types';
const initialState = {
firstName: '',
lastName: '',
email: '',
password: '',
repeatPassword: ''
};
const signUpReducer = (state = initialState, action) => {
switch(action.type) {
case Actions.CHANGE_FIRST_NAME_INPUT:
return Object.assign({}, state,
{ firstName: action.firstName }
);
case Actions.CHANGE_LAST_NAME_INPUT:
return Object.assign({}, state,
{ lastName: action.lastName }
);
case Actions.CHANGE_EMAIL_INPUT:
return Object.assign({}, state,
{ email: action.email }
);
case Actions.CHANGE_PASSWORD_INPUT:
return Object.assign({}, state,
{ password: action.password }
);
case Actions.CHANGE_REPEAT_PASSWORD_INPUT:
return Object.assign({}, state,
{ repeatPassword: action.password }
);
default:
return state;
}
}
export default signUpReducer;
Store
import { createStore, combineReducers } from 'redux';
import signInReducer from '../reducers/SignIn';
import signUpReducer from '../reducers/SignUp';
import profileReducer from '../reducers/Profile';
const rootReducer = combineReducers({
signIn: signInReducer,
signUp: signUpReducer,
profile: profileReducer
});
const configureStore = () => {
return createStore(rootReducer);
}
export default configureStore;
As you can see there are some common actions like CHANGE_EMAIL_INPUT & CHANGE_PASSWORD_INPUT and I dont want them to be triggered together. One way I can figure out is to change the name of actions and make then more specific to screen but this doesn't sound good. Another could be to wrap reducers so that we know what is being called but not getting an idea on the wrapper.
Any suggestions.
You should not reuse the same action name between 2 reducers, to avoid unintended effects, use different names.
For example
Actions.SIGNUP_ CHANGE_EMAIL_INPUT
and
Actions.SIGNIN_ CHANGE_EMAIL_INPUT
Otherwise, you can merge your 2 reducers, adding a state to know from which screen this change emerged.
well, i solved it by creating a wrapper for the reducers.
Store
function createNamedWrapperReducer(reducerFunction, reducerName) {
return (state, action) => {
const isInitializationCall = state === undefined;
const shouldRunWrappedReducer = reducerName(action) || isInitializationCall;
return shouldRunWrappedReducer ? reducerFunction(state, action) : state;
}
}
const rootReducer = combineReducers({
// signIn: signInReducer,
// signUp: signUpReducer,
// profile: profileReducer
signIn: createNamedWrapperReducer(signInReducer, action => action.name === 'signIn'),
signUp: createNamedWrapperReducer(signUpReducer, action => action.name === 'signUp'),
profile: createNamedWrapperReducer(profileReducer, action => action.name === 'profile'),
});
Screen
onChangeEmail: (email) => { dispatch({name: 'signIn', type: Actions.CHANGE_EMAIL_INPUT, email: email}) },
I'm trying to access a piece of state from a different reducer in my action creator. I know there's a ton of questions out there like this already, but so far, I'm still hung up on why this isn't working even after reading them. My action creator takes a JSON from Firebase and filters it according to another piece of state called 'search' from the searching reducer. Here's that code:
Action Creator:
export const searchResult = () => {
const { currentUser } = firebase.auth();
return (dispatch, getState) => {
firebase.database().ref(`/users/${currentUser.uid}/entries`)
.orderByChild('uid')
.on('value', snapshot => {
const myObj = snapshot.val();
const { search } = getState().searching;
const list = _.pickBy(myObj, (((value) =>
value.make.indexOf(search) !== -1 ||
value.model.indexOf(search) !== -1));
dispatch({ type: SEARCH_RESULT_SUCCESS, payload: list });
});
};
};
And here's the Action Creator for the text input of the search filter:
export const searchChanged = (text) => {
return {
type: SEARCH_CHANGED,
payload: text
};
};
And here's the reducer for searchResult called entryReducer:
import {
SEARCH_RESULT_SUCCESS,
ENTRY_FETCH_SUCCESS,
SOLD_RESULT_SUCCESS
} from '../actions/types';
const INITIAL_STATE = [];
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case ENTRY_FETCH_SUCCESS:
return action.payload;
case SEARCH_RESULT_SUCCESS:
return action.payload;
case SOLD_RESULT_SUCCESS:
return action.payload;
default:
return state;
}
};
And here's the searchReducer:
import {
SEARCH_CHANGED,
} from '../actions/types';
const INITIAL_STATE = {
search: '',
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case SEARCH_CHANGED:
return { ...state, search: action.payload };
default:
return state;
}
};
And here's the combineReducers function:
import { combineReducers } from 'redux';
import AuthReducer from './AuthReducer';
import EntryFormReducer from './EntryFormReducer';
import EntryReducer from './EntryReducer';
import SearchReducer from './SearchReducer';
import PasswordReducer from './PasswordReducer';
export default combineReducers({
auth: AuthReducer,
entryForm: EntryFormReducer,
employees: EntryReducer,
searching: SearchReducer,
pw: PasswordReducer
});
Here's where I call searchResult():
class EmployeeList extends Component {
componentWillMount() {
this.props.searchResult();
And my mapStateToProps():
const mapStateToProps = (state) => {
const employees = _.map(state.employees, (val, uid) => {
return { ...val, uid };
});
return { employees };
};
export default connect(mapStateToProps, { searchResult, entryClear,
logoutUser })(EmployeeList);
And here's where searchChanged() is called inside the component Search:
import React, { Component } from 'react';
import { View } from 'react-native';
import { connect } from 'react-redux';
import { Icon } from 'react-native-vector-icons';
import { searchChanged, searchResult } from '../actions';
import Card from './common/Card';
import CardSection from './common/CardSection';
import Input from './common/Input';
class Search extends Component {
onSearchChange(text) {
this.props.searchChanged(text);
searchResult();
}
The code runs just fine, but nothing changes when I type into the search filter text input. I can see that my piece of state called 'search' from the 'searching' reducer gets updated. But my action creator 'searchResult' isn't accessing it to filter by it. Any help would be appreciated...I'm new to redux.