Redux: accessing dictionary as state object - javascript

I have implemented a shopping cart using Redux, I have used a dictionary as state object (product id being the key and quantity in cart being the value). Here is how my cart.js looks like:
import React from 'react';
import ReactDOM from 'react-dom';
export const AddItemToCart = (productID) => {
return {
type: 'ADDITEMTOCART',
productID
}
}
export const DeleteItemFromCart = (productID) => {
return {
type: 'DELETEITEMFROMCART',
productID
}
}
export const Counter = (state = {}, action) => {
switch (action.type) {
case 'ADDITEMTOCART':
console.log(action);
return {
...state,
[action.productID]: ( state[action.productID] || 0 ) + 1
}
case 'DELETEITEMFROMCART':
return {
...state,
[action.productID]: ( state[action.productID] || 1 ) - 1
}
}
}
I'm adding an item from App.js like this:
return products.map(products =>
<div key={products.ProductID}>
<h2>{products.ProductName}</h2>
<h2>{products.ProductDescription}</h2>
<h2>{products.ProductQuantity} units available</h2>
<button onClick={() => { store.subscribe(() => console.log(store.getState()));
store.dispatch(AddItemToCart(products.ProductID));}}>Add to cart</button>
</div>
Everything is working just fine but the problem is, I can't render the contents of the cart for user to see. I have tried:
function ShowCartContents() {
var items = Object.keys(store.getState()).map(function(key){
return store.getState()[key];
});
return (
<div>
<h2>{items}</h2>
</div>
);
}
This function throws exception when called:
TypeError: Cannot convert undefined or null to object
Clearly the store itself is not null or undefined, because the change of state is successfully printed to the browser console. So, how would I access all the values in dictionary without keys? And how would I access one specific value by key? Any advise would be highly appreciated. Thanks.

Your Counter reducer has no default case, so your state will be undefined on the first render.
That's the source of your error "TypeError: Cannot convert undefined or null to object".
You need to return the existing state when neither action matches. Every reducer needs a default case because they will be called with actions like the {type: '##redux/INIT'} action which is used to initialize the store.
default:
return state;
You are trying to access the store directly with store.subscribe(), store.getState() and store.dispatch(). This is not the correct way to interact with a Redux store in React. You should use the react-redux package.
You want to wrap your entire app in a Provider component that provides the store instance. Something like this:
import { render } from "react-dom";
import { Provider } from "react-redux";
import App from "./components/App";
import store from "./store";
const rootElement = document.getElementById("root");
render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
In your components, use the hook useSelector to select values from the state and useDispatch to access the dispatch function. (You can also use the connect higher-order component, but the hooks are preferred).
I'm not sure if this reducer is your entire state or if you are using combineReducers and have multiple reducers like cart, products, etc. This selector is assuming that it's the entire state.
function ShowCartContents() {
const productIds = useSelector(state => Object.keys(state))
return (
<div>
<h2>Ids In Cart: {productIds.join(", ")}</h2>
</div>
);
}
function ProductsList({ products }) {
const dispatch = useDispatch();
return (
<div>
{products.map((product) => (
<div key={product.ProductID}>
<h2>{product.ProductName}</h2>
<h2>{product.ProductDescription}</h2>
<h2>{product.ProductQuantity} units available</h2>
<button onClick={() => dispatch(AddItemToCart(product.ProductID))}>
Add to cart
</button>
</div>
))}
</div>
);
}

Related

Redux/React: Cannot read property 'type' of undefined when changing activePage

Heyo this is my first time using react/redux for a project and so I've been migrating my "navigation" to a redux style implementation where I (at the start here) want to change a store state (activePage) on the click of a button, and then my App should render whatever page is active through that activePage state.
I'm stuck (should emphasize that I'm not sure what is overboard/overwriting stuff or missing for this, I've followed a few online tutorials for action/reducer/store type stuff (and I was going to do a dispatch call but it seems like I can call changePage right from the button click instead of dispatching (had problems implementing the dispatch)) and I've been banging my head against the desk as to how action is undefined when import it...perhaps I'm not looking at it correctly...am I missing any data that would help diagnose this error?:
TypeError: Cannot read property 'type' of undefined
allReducers
.../client/src/redux/reducer/index.js:9
6 | activePage: 'HomePage',
7 | }
8 | function allReducers(state = initState, action){
9 | switch(action.type){
10 | case CHANGE_PAGE:
11 | return{
12 | ...state,
loginButton.js
const mapDispatchToProps = (state) => {
return {
changePage : state.activePage
}
};
function LoginButtonThing (){
return(
<div>
<button onClick={() =>(changePage('loginPage'))}>Login Page</button>
</div>
)
}
//export default LoginButtonThing;
export default connect(null,mapDispatchToProps)(LoginButtonThing);
actions.js
import {
CHANGE_PAGE,
} from "../constants/action-types";
export const changePage = (activePage) => ({
type: CHANGE_PAGE,
payload: {
activePage,
},
});
action-types.js
export const CHANGE_PAGE = "CHANGE_PAGE";
reducer/index.js
import {CHANGE_PAGE} from "../constants/action-types";
import {changePage} from "../actions/actions"
const initState = {
activePage: 'HomePage',
}
function allReducers(state = initState, action){
switch(action.type){
case CHANGE_PAGE:
return{
...state,
activePage :action.payload.activePage,
};
}
}
//const store = createStore(rootReducer);
export default allReducers;
App.js
class App extends React.Component {
render() {
//this.props.activePage = 'HomePage';
let renderedPage;
if (this.props.changePage === "LoginPage") renderedPage = <LoginPage/>;
else if (this.props.changePage === "HomePage") renderedPage = <HomePage/>;
else renderedPage = <HomePage/>;
//defaultStatus:activePage = "HomePage";
return (
<div id="App">
{renderedPage}
</div>
);
}
}
index.js
import {createStore, combineReducers} from 'redux';
import allReducers from "./redux/reducer"
//import LoginPage from "./loginPage";
import {Provider} from "react-redux";
const store = createStore(
allReducers,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
ReactDOM.render(
<Provider store = {store}>
<React.StrictMode>
<BrowserRouter>
<Route exact path="/" component = {HomePage}>
<Route path= "/loginPage" component ={LoginPage} />
</Route>
</BrowserRouter>
<App />
</React.StrictMode>
</Provider>,
document.getElementById('root')
);
Missing Default Case
Your reducer needs to return state if an unknown action is dispatched, such as the "init" action which sets your state to the initial value.
function allReducers(state = initState, action) {
switch (action.type) {
case CHANGE_PAGE:
return {
...state,
activePage: action.payload.activePage
};
default:
return state;
}
}
Mixing up State & Dispatch
The connect HOC takes two arguments: mapStateToProps and mapDispatchToProps. I think the names are self-explanatory. So why does your mapDispatchToProps function take state as an argument instead of dispatch?
If using connect, the LoginButtonThing should access a version of changePage which is already bound to dispatch from its props. You can use the action object shorthand like this:
import { connect } from "react-redux";
import { changePage } from "../store/actions";
function LoginButtonThing({ changePage }) {
return (
<div>
<button onClick={() => changePage("loginPage")}>Login Page</button>
</div>
);
}
export default connect(null, { changePage })(LoginButtonThing);
But you should use hooks, like this:
import { useDispatch } from "react-redux";
import { changePage } from "../store/actions";
export default function LoginButtonThing() {
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(changePage("loginPage"))}>Login Page</button>
</div>
);
}
Inconsistent Naming
This doesn't actually navigate to the LoginPage component! That's because LoginButtonThing updated the page to "loginPage" (lowercase), but App is looking for the string "LoginPage" (uppercase).
Don't Do This
There is a lot that's wrong with your code and a lot that's just sort of "dated" and we have better ways now, like hooks and Redux-Toolkit.
I actually think that moving navigation state into Redux like you are trying to do is not a good idea and I would recommend that you stick with react-router. You can read here why you don't need or want to do this.

How to change th value of a state property?

I am making a small application that obtains data, is displayed in the DOM, and chooses an item that displays the information of the chosen user, I handle all this through the state manager called UserState, where I also add the methods to display the users. And then as a component, I have UserList and UserProfile.
This is how should work, Capture 1
UserState.js
import React, {useState} from 'react';
import UserContext from './UserContext';
import axios from 'axios';
function UserState(props) {
const initialState = {
users:[],
selectedUser:null
}
const [state, setState] = useState(initialState)
const getUsers = async() =>{
const res = await axios.get("https://reqres.in/api/users")
const data = res.data.data
setState({users:data,
selectedUser:null})
}
const getProfile = async (id) => {
const res = await axios.get("https://reqres.in/api/users/"+id)
const {data} = await res.data;
console.log('Item Selected:',data)
console.log(setState({selectedUser:data}))
}
return (
<UserContext.Provider
value={{
users:state.users,
selectedUser: state.selectedUser,
getUsers,
getProfile
}}
>
{props.children}
</UserContext.Provider>
)
}
export default UserState
I export That state and its methods through the Hook useContext, the problem starts when I try to choose a user, and the console shows me the following error.
UserList.js
import React,{useContext,useEffect} from 'react'
import UserContext from "../context/User/UserContext"
function UserList(props) {
const userContext = useContext(UserContext)
useEffect(() => {
userContext.getUsers();
},[])
return (
<div>
<h1>UserList</h1>
{userContext.users.map(user=>{
return(
<a
key={user.id}
href="#!"
onClick={()=> userContext.getProfile(user.id)}
>
<img src={user.avatar} alt="" width="70"/>
<p>{user.first_name} {user.last_name}</p>
<p>{user.email}</p>
</a>)
}): null}
</div>
)
}
export default UserList
Profile.js
import React,{useContext} from 'react'
import UserContext from '../context/User/UserContext'
function Profile() {
const {selectedUser} = useContext(UserContext)
return (
<>
<h1>Profile</h1>
{selectedUser ?
(
<div>
<h1>Selected Profile</h1>
{/* <img
src={selectedUser.avatar}
alt=""
style={{width:150}}
/> */}
</div>
):(<div>
No User Selected
</div>)}
</>
)
}
export default Profile
Console Error
I tried to change the value of selectedUser but every time the console shows me that error.
In your getProfile function, you should use setState like that.
setState({...state, selectedUser:data })
If you use setState({selectedUser:data }) then users is removed from state.
It looks like it's an issue with the asynchronous portion of your code. Initially, you have no state.users object, so when you attempt to use the properties of the state.users object in the line like {userContext.users.map(user=>{... there is nothing to map, and since map uses the length property, you are getting that error. You should check first to see if that component has a userContext.users property and that the length is greater than or equal to 1 before attempting to map.
You're using the useState hook in a slightly odd way too, which confuses things a bit. Typically when using the useState hook, each element will have its own state rather than setting a single state to handle multiple elements. In this one, you'd set two separate states, one called users and one called selectedUser and set them independently. Otherwise you can have some odd re-renders.
By the way, React error codes are very descriptive. It tells you that state.users is undefined, that it can't access property map of undefined, and that it's on line 13 of your UserList.js component. All of which is true.

State change isn't re-rendering component after axios data fetch in react-redux useSelector()

I'm trying to fetch and display data on the initial load of an application using react and redux. But the component that should display the data does not have the data by the time it is rendered. It eventually gets the data but doesn't re-render for some reason.
Here are my two components in question:
App.js
import React, {useEffect} from 'react';
import './App.css';
import RecordList from './components/RecordList';
import CreateRecord from './components/CreateRecord';
import {useDispatch} from 'react-redux';
import { initRecords } from './actions/recordActions';
function App() {
const dispatch = useDispatch();
// Gets initial record list.
useEffect(() => {
dispatch(initRecords());
}, [dispatch])
return (
<div className="App">
<CreateRecord />
<RecordList/>
</div>
);
}
export default App;
RecordList.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
export default function RecordList() {
const dispatch = useDispatch();
const records = useSelector(state=>state);
console.log('state: ', records)
return (
<div>
<h3>Albums</h3>
{records.map(record =>
<div key={record.id}>
{record.albumName} by {record.artist}
</div>
)}
</div>
)
}
The issue I'm having is that initial data fetch in App.js isn't returning fast enough by the time the RecordList.js component is rendered. So in RecordList.js this bit throws an error saying map is not a function or cannot map on undefined:
{records.map(record =>
<div key={record.id}>
{record.albumName} by {record.artist}
</div>
)}
The component does eventually get the data if you comment out the JSX throwing the error. Initially it logs records as undefined but after a second it logs it with correct values.
Here are my reducer and actions:
recordActions.js
import recordService from '../services/records';
export const initRecords = () => {
return async dispatch => {
const records = await recordService.getAll();
console.log('from actions: ', records);
dispatch({
type: 'INIT_RECORDS',
data: records
})
};
}
reducer
const recordReducer = (state = [], action) => {
console.log('state now: ', state)
console.log('action', action)
switch(action.type) {
case 'CREATE':
return [...state, action.data];
case 'INIT_RECORDS':
return action.data;
default: return state;
}
}
export default recordReducer
Lastly, here is where I am making the axios call:
service
const getAll = async () => {
const response = await axios.get('someapi.com/records');
return response.data;
}
I've tried to conditionally render both the entire recordsList component and the records.map but the conditions only check once on the first load and never check again.
From my understanding, useSelector() should re-render the component when there's a state change, is it possible the state is just being mutated and not changed and how can I fix this?
Figured it out! Turns out in useSelector(state=>state) I needed to change it to useSelector(state=>state.records.Items) to get to the array.

How to update React component after changing state through redux?

I was learning React and Redux and while doing that I decided to make webpage with a button which on clicking would change the state. Below the button I wanted to display the current state in a different component. Though the button on clicking changes the state, but it is not getting reflected in the component. Here is my code:
App.js
import React from 'react'
import Name from './Name'
import {changeName} from './Action';
export default function App () {
return (
<div>
<button onClick={changeName}>Click me</button>
<Name />
</div>
)
}
Name.js
import React from 'react'
import {store} from './Store'
function Name(props) {
return (
<div>
My name is: {store.getState()}
</div>
)
}
export default Name
Store.js
import { createStore } from 'redux';
import {reducer} from './Reducer';
export const store = createStore(reducer, 'Tarun');
Action.js
import {store} from './Store';
export const changeName = () => {
if (store.getState() === "Tarun"){
store.dispatch({ type: 'name', payload: 'Subhash' });
}
else{
store.dispatch({ type: 'name', payload: 'Tarun' });
}
}
Reducer.js
export const reducer = function(state, action) {
if (action.type === 'name') {
return action.payload;
}
return state;
};
When I click the button, The text inside the Name component does not change. What is the issue?
You need to set up your reducer and initial store properly following the Redux documentation.
You're missing a Provider, which will provide your store to your application.
const store = createStore(reducer, applyMiddleware(thunk));
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
Now, your store is available to your components.
Your reducer needs an initial state too and you're always supposed to return an updated copy of your state. That said, don't change the state directly, but make a copy, change it, then return that copy.
const initialState = {
name: ""
};
const reducer = function(state = initialState, action) {
if (action.type === "name") {
return { ...state, name: action.payload };
} else {
return state;
}
};
export default reducer;
You might have noticed that I added a middleware to your store, and that's because it's usually the way to go when accessing your current reducer's state in your actions. That said, I installed redux-thunk for that, so in your action, you can have something like this:
export const changeName = () => {
return (dispatch, getState) => {
if (getState().name === "Tarun") {
dispatch({ type: "name", payload: "Subhash" });
} else {
dispatch({ type: "name", payload: "Tarun" });
}
};
};
Now, with your store being provided to your app, your reducer being done and your actions being ready to go, you can connect different components to your reducer.
You use the high order component in react-redux called connect for that. For example, in your Name component, we can connect the name to be displayed to your reducer by mapping your state to the component's props:
function Name(props) {
return <div>My name is: {props.name}</div>;
}
const mapStateToProps = state => {
return {
name: state.name
};
};
export default connect(mapStateToProps)(Name);
The nice thing here is that you can also leave the first parameter in the connect high order component empty and just pass the second, which would be the dispatch functions. Well, that's what you would do in your App component, you would connect it to the changeName action.
function App(props) {
return (
<div>
<button onClick={props.changeName}>Click me</button>
<Name />
</div>
);
}
const mapDispatchToProps = dispatch => {
return {
changeName: () => dispatch(changeName())
};
};
export default connect(
null,
mapDispatchToProps
)(App);
Now, when App dispatches a changeName action, your reducer state will be updated and the other components that are connected to the reducer's state will re-render.
Summary: Try to think of your store as an empty jar of candies. Your jar starts empty, but different actions could change what's inside the jar. On top of that, different people in the house that know where the jar is can go get some candy. Translating to your problem, your app begins with an empty name and you have an action that sets up a name. The components that know where to find that name by being connected to your reducer will know when that name changes and will get the updated name.
The final code can be found here:
The only way your name component will rerender is its props or state change, or if a parent component rerenders. Making a change in redux will not automatically do this. In order to see changes to the state, you'd need to subscribe to those changes. You could do this yourself, but a far better solution is to use react-redux, which is designed for connecting react components to redux stores.
For example, you'd add a provider to your app:
import { Provider } from 'react-redux';
import { store } from './Store'
export default function App () {
return (
<Provider store={store}>
<div>
<button onClick={changeName}>Click me</button>
<Name />
</div>
</Provider>
)
}
And then you'd use connect with your Name component:
import { connect } from 'react-redux';
function Name(props) {
return (
<div>
My name is: {props.name}
</div>
)
}
const mapStateToProps = (state) => {
return { name: state };
}
export default connect(mapStateToProps)(Name)

React Hooks: Dispatch an action on componentDidMount

I have three pages, PageA, PageB and PageC, that contain a form element formField.
State in globalReducer.js
import { fromJS } from 'immutable';
const initialState = fromJS({
userInteractionBegun: false,
pageActive: '',
refreshData: true,
})
I want to dispatch an action that sets pageActive to corresponding page value(One of A, B or C) when the component(page) mounts and refreshes formField to blank if userInteractionBegun === false.
For every page component, to get pageActive state in props from globalReducer, I do,
function PageA(props) {
//.....
}
// globalState is defined in conigureStore, I am using immutable.js. Link provided below this code.
const mapStateToProps = state => ({
pageActive: state.getIn(['globalState', 'pageActive']),
})
export default connect(mapStateToProps, null)(PageA);
Link to immutable.js getIn()
store.js
import globalReducer from 'path/to/globalReducer';
const store = createStore(
combineReducers({
globalState: globalReducer,
//...other reducers
})
)
I want to abstract the logic to update pageActive every time a component(page) mounts.
I know how to abstract this logic using an HOC, but I don't know how to do it using react hooks, so that every time pageA, pageB or pageC mounts, an action to setPageActive is dispatched and formField is set to blank if userInteractionBegun is false.
For instance, I would do in pageA.js
import usePageActive from 'path/to/usePageActive';
const [pageActive, setPageActive] = useReducer(props.pageActive);
usePageActive(pageActive);
Then in usePageActive.js
export default usePageActive(pageActive) {
const [state, setState] = useState(pageActive);
setState(// dispatch an action //)
}
I haven't had much time to dip my toes into react hooks yet, but after reading the docs and playing with it for a minute, I think this will do what you're asking. I'm using built-in state here, but you could use redux or whatever else you like in the effect. You can see a working example of this code here The trick is using a hook creator to create the custom hook. That way the parent and children can keep a reference to the same state without the useEffect affecting the parent.
import React, { useState, useEffect } from 'react';
import ReactDOM from "react-dom";
const activePageFactory = (setActivePage) => (activePage) => {
useEffect(() => {
setActivePage(activePage)
return () => {
setActivePage('')
}
}, [activePage])
return activePage
}
function App() {
const [activePage, setActivePage] = useState('');
const [localPage, setLocalPage] = useState('Not Selected');
const selectedPage = () => {
switch(localPage) {
case 'A':
return <PageA useActivePage={activePageFactory(setActivePage)} />
case 'B':
return <PageB useActivePage={activePageFactory(setActivePage)} />
default:
return null;
}
}
return (
<div>
<p>Active page is {activePage}</p>
<button onClick={() => setLocalPage('A')}>
Make A Active
</button>
<button onClick={() => setLocalPage('B')}>
Make B Active
</button>
{
selectedPage()
}
</div>
);
}
function PageA({useActivePage}) {
useActivePage('A');
return (
<div>
<p>I am Page A</p>
</div>
)
}
function PageB({useActivePage}) {
useActivePage('B');
return (
<div>
<p>I am Page B</p>
</div>
)
}

Categories

Resources