I have code as shown below. My question is regarding react and redux usage. I am not able to get a state "categories" via mapstate method. It is showing undefined when I try to console categories via this.props.categories in my Product.js. I have attached product.js file which has reducer and another is my main component Product.js.
Here is my Product.js code:
import React from 'react'
import {connect} from 'react-redux'
import withStyles from 'isomorphic-style-loader/lib/withStyles'
import s from './Product.css'
import { Select, Button, Table, Tooltip, InputNumber, Card} from 'antd'
import ProductForm from './ProductForm'
import {withCurrency} from '../../components'
import debounce from 'lodash/debounce'
import {
changeSelectedProduct,
getProducts,
addProduct,
changeMarkupFactor,
updateProduct,
currencyClicked,
editRetailPrice,
retailPriceUpdated,
handleChange
} from '../../reducers/product'
const children = ["Home","Health","Beauty","Pregnancy","Kits","Animals","Travel","Cooking"]
class Product extends React.Component {
constructor(props) {
super(props)
this.getProducts = debounce(props.getProducts, 800);
}
componentWillMount() {
// TODO get initial list of popular products
this.props.getProducts()
}
handleChange = (values) => {
console.log(values)
let Values = children.filter( (eachone,index) =>{
var includes = values.includes(`${index}`)
if(includes){
return true
}
}
)
this.props.handleChange(Values)
}
render() {
const {
products,
loading,
selectedProduct,
onSubmit,
formRef,
markupFactor,
changeMarkupFactor,
changeSelectedProduct,
currencies,
totalPrice,
currency,
clearForms,
addRetailPrice,
currencyClicked,
editRetailPrice,
retailPriceUpdated,
categories
} = this.props;
console.log('entire state', this.props.state)
const columns = [
{
key: 'name',
dataIndex: 'name',
width: 130,
className: s.nameColumn,
},
{
key: 'price',
dataIndex: 'price',
className: s.priceColumn,
render: (price, row) => {
if (row.id === 'retail-price' && addRetailPrice) {
return (
<InputNumber
value={totalPrice.retail_price[currency.key]}
onChange={(value) => editRetailPrice(value, currency.key)}
defaultValue={price[currency.key]}
onBlur={() => retailPriceUpdated(currency.key)}
/>
);
}
return (
<Tooltip
placement='topRight'
title={
currencies.map(item =>
currency.key !== item.key ? (
<div key={item.key}>
{withCurrency(item, price[item.key])}
</div>
) : null
)
}
>
<div onClick={() => currencyClicked(row)}>
{withCurrency(currency, price[currency.key])}
</div>
</Tooltip>
)
}
}
]
let data = []
if (totalPrice) {
data = [
{id:'receipe', name: 'Recipe', price: totalPrice.recipe.total_price},
{id:'container', name: 'Container', price: totalPrice.container.total_price},
{id:'wholesale-price', name: 'Wholesale Price', price: totalPrice.wholesale_price},
{id:'retail-price', name: 'Retail Price', price: totalPrice.retail_price},
]
}
return (
<Card
className={s.container}
title={'Product'}
bodyStyle={{minHeight: 409}}
extra = {<a onClick={clearForms}>{'Clear'}</a>}
>
{/* <div className={s.container}> */}
{/* <div className={s.headerWrapper}>
<h3 className={s.header}>{'Product'}</h3> */}
{/*use clear here below */}
{/* <a onClick={clearForms}>{'Clear'}</a>
</div> */}
<div className={s.containerSearchWrapper}>
<Select
className = {s.search}
showSearch
allowClear
value={selectedProduct ? `${selectedProduct.id}` : undefined}
className={s.productSearch}
placeholder='Search'
notFoundContent={loading.products ? <Spin size='small'/> : null}
filterOption={false}
onSearch={(search) => this.getProducts({search})}
onChange={(id) => {
const newProduct = products.find(item => item.id === +id)
changeSelectedProduct(newProduct)
}}
>
{products.map(item =>
<Select.Option key={item.id}>{item.name}</Select.Option>
)}
</Select>
</div>
<ProductForm
markupFactor={markupFactor}
changeMarkupFactor={changeMarkupFactor}
ref={formRef}
children={children}
handleChange={this.handleChange}
/>
<Table
className={s.table}
columns={columns}
dataSource={data}
loading={loading.totalPrice}
size='small'
rowKey={(record, i) => i}
pagination={false}
showHeader={false}
locale={{emptyText: 'Total price'}}
/>
<div className={s.actions}>
<Button
onClick={() => onSubmit('update')}
loading={loading.updatingProduct}
disabled={!selectedProduct}
>
{'Save'}
</Button>
<Button
onClick={() => onSubmit('add')}
loading={loading.addingProduct}
>
{'Create new'}
</Button>
</div>
{/* </div> */}
</Card>
)
}
}
const mapState = state => ({
...state.product,
currency: state.global.currency,
currencies: state.global.currencies,
categories: state.global.categories,
})
const mapDispatch = {
getProducts,
addProduct,
updateProduct,
changeMarkupFactor,
changeSelectedProduct,
currencyClicked,
editRetailPrice,
retailPriceUpdated,
handleChange,
}
export default connect(mapState, mapDispatch)(withStyles(s)(Product))
Here is my product.js code
import createReducer, {RESET_STORE} from '../createReducer'
import {getToken} from './user'
import qs from 'query-string'
import _ from 'lodash';
import {message} from 'antd'
import messages from '../messages'
import {getFieldValue} from '../utils'
// ------------------------------------
// Constants
// ------------------------------------
export const GET_PRODUCT_REQUEST = 'Product.GET_PRODUCT_REQUEST'
export const GET_PRODUCT_SUCCESS = 'Product.GET_PRODUCT_SUCCESS'
export const GET_PRODUCT_FAILURE = 'Product.GET_PRODUCT_FAILURE'
export const ADD_PRODUCT_REQUEST = 'Product.ADD_PRODUCT_REQUEST'
export const ADD_PRODUCT_SUCCESS = 'Product.ADD_PRODUCT_SUCCESS'
export const ADD_PRODUCT_FAILURE = 'Product.ADD_PRODUCT_FAILURE'
export const UPDATE_PRODUCT_REQUEST = 'Product.UPDATE_PRODUCT_REQUEST'
export const UPDATE_PRODUCT_SUCCESS = 'Product.UPDATE_PRODUCT_SUCCESS'
export const UPDATE_PRODUCT_FAILURE = 'Product.UPDATE_PRODUCT_FAILURE'
export const GET_TOTAL_PRICE_REQUEST = 'Product.GET_TOTAL_PRICE_REQUEST'
export const GET_TOTAL_PRICE_SUCCESS = 'Product.GET_TOTAL_PRICE_SUCCESS'
export const GET_TOTAL_PRICE_FAILURE = 'Product.GET_TOTAL_PRICE_FAILURE'
export const CHANGE_SELECTED_PRODUCT = 'Product.CHANGE_SELECTED_PRODUCT'
export const CHANGE_MARKUP_FACTOR = 'Product.CHANGE_MARKUP_FACTOR'
export const CHANGE_RETAIL_PRICE = 'Product.CHANGE_RETAIL_PRICE';
export const EDITING_RETAIL_PRICE = 'Product.EDITING_RETAIL_PRICE';
export const RETAIL_PRICE_UPDATED = 'Product.RETAIL_PRICE_UPDATED';
export const CLEAR = 'Product.CLEAR'
export const ADD_CATEGORIES = 'Product.ADD_CATEGORIES'
// ------------------------------------
// Actions
// ------------------------------------
// ------------------------------------
// Actions
// ------------------------------------
export const changeSelectedProduct = (selectedProduct) => (dispatch, getState) => {
dispatch({type: CHANGE_SELECTED_PRODUCT, selectedProduct})
// dispatch(changeSelectedComponents(selectedProduct ? selectedProduct.components : []))
}
export const currencyClicked = (row) => (dispatch) => {
if (!row.id === 'retail-price') return;
dispatch({type: CHANGE_RETAIL_PRICE})
}
export const editRetailPrice = (value, currencyKey) => (dispatch, getState) => {
const { totalPrice } = getState().product;
totalPrice.retail_price[currencyKey] = value;
dispatch({type: EDITING_RETAIL_PRICE, totalPrice});
}
export const retailPriceUpdated = (currencyKey) => (dispatch, getState) => {
const { totalPrice } = getState().product;
const markupFactor = parseFloat(totalPrice.retail_price[currencyKey] / totalPrice.wholesale_price[currencyKey]).toFixed(2);
dispatch({type: RETAIL_PRICE_UPDATED, totalPrice});
dispatch({type: CHANGE_MARKUP_FACTOR, markupFactor});
}
export const getProducts = (params = {}) => (dispatch, getState, {fetch}) => {
dispatch({type: GET_PRODUCT_REQUEST, params})
const {token} = dispatch(getToken())
const {search, ordering} = getState().product
return fetch(`/pands/products/?${qs.stringify({
search,
ordering,
})}`, {
method: 'GET',
token,
success: (res) => dispatch({type: GET_PRODUCT_SUCCESS, res}),
failure: (err) => dispatch({type: GET_PRODUCT_FAILURE}),
})
}
export const addProduct = (values) => (dispatch, getState, {fetch}) => {
dispatch({type: ADD_PRODUCT_REQUEST})
const {token} = dispatch(getToken())
const {currencies} = getState().global
const {selectedRecipe} = getState().recipe
const {selectedContainer} = getState().container
return fetch(`/pands/products/`, {
method: 'POST',
body: {
...values,
recipe: selectedRecipe.id,
container: selectedContainer.id,
currencies: currencies.map(item => item.key),
},
token,
success: (selectedProduct) => {
dispatch({type: ADD_PRODUCT_SUCCESS, selectedProduct})
message.success(messages.addProductSuccess)
},
failure: (err) => {
dispatch({type: ADD_PRODUCT_FAILURE})
message.error(messages.addProductError)
},
})
}
export const updateProduct = (values) => (dispatch, getState, {fetch}) => {
dispatch({type: UPDATE_PRODUCT_REQUEST})
const {token} = dispatch(getToken())
const {currencies} = getState().global
const {selectedProduct} = getState().product
const {selectedRecipe} = getState().recipe
const {selectedContainer} = getState().container
return fetch(`/pands/products/${selectedProduct.id}/`, {
method: 'PATCH',
body: {
...values,
recipe: selectedRecipe.id,
container: selectedContainer.id,
currencies: currencies.map(item => item.key),
},
token,
success: (selectedProduct) => {
dispatch({type: UPDATE_PRODUCT_SUCCESS, selectedProduct})
message.success(messages.updateProductSuccess)
},
failure: (err) => {
dispatch({type: UPDATE_PRODUCT_FAILURE})
message.error(messages.updateProductError)
},
})
}
export const getTotalPrice = () => (dispatch, getState, {fetch}) => {
const {token} = dispatch(getToken())
dispatch({type: GET_TOTAL_PRICE_REQUEST})
const {currencies} = getState().global
const {markupFactor} = getState().product
const {fields: {ingredients}} = getState().rawMaterials
const {fields: {components}} = getState().components
return fetch(`/pands/products/calculate/`, {
method: 'POST',
token,
body: {
recipe: {
ingredients: getFieldValue(ingredients),
},
container: {
components: getFieldValue(components),
},
markup_factor: +markupFactor,
currencies: currencies.map(item => item.key),
},
success: (totalPrice) => dispatch({type: GET_TOTAL_PRICE_SUCCESS, totalPrice}),
failure: (error) => dispatch({type: GET_TOTAL_PRICE_FAILURE, error}),
})
}
export const changeMarkupFactor = (markupFactor) => (dispatch, getState) => {
dispatch({type: CHANGE_MARKUP_FACTOR, markupFactor})
dispatch(getTotalPrice())
}
export const clear = () => ({type: CLEAR})
// -----------------------------------
export const handleChange = (values) => (dispatch) => {
dispatch({type: ADD_CATEGORIES,values});
}
// ------------------------------------
// Reducer
// -----------------------------------
const initialState = {
loading: {
products: false,
addingProduct: false,
updatingProduct: false,
totalPrice: false,
},
products: [],
selectedProduct: null,
error: null,
totalPrice: null,
markupFactor: 1.3,
addRetailPrice: false,
categories:[],
}
export default createReducer(initialState, {
[GET_PRODUCT_REQUEST]: (state, {params}) => ({
search: _.has(params, 'search') ? params.search : state.search,
ordering: params.sorter ? `${params.sorter.order === 'descend' ? '-' : ''}${params.sorter.field}` : state.ordering,
filters: params.filters || state.filters,
loading: {
...state.loading,
products: false,
},
}),
[GET_PRODUCT_SUCCESS]: (state, {res: {results}}) => ({
products: results,
loading: false,
}),
[GET_PRODUCT_FAILURE]: (state, action) => ({
loading: false,
}),
[ADD_PRODUCT_REQUEST]: (state, action) => ({
loading: {
...state.loading,
addingProduct: true,
},
}),
[ADD_PRODUCT_SUCCESS]: (state, {selectedProduct}) => ({
selectedProduct,
loading: {
...state.loading,
addingProduct: false,
},
}),
[ADD_PRODUCT_FAILURE]: (state, action) => ({
loading: {
...state.loading,
addingProduct: false,
},
}),
[UPDATE_PRODUCT_REQUEST]: (state, action) => ({
loading: {
...state.loading,
updatingProduct: true,
},
}),
[UPDATE_PRODUCT_SUCCESS]: (state, {selectedProduct}) => ({
selectedProduct,
loading: {
...state.loading,
updatingProduct: false,
},
}),
[UPDATE_PRODUCT_FAILURE]: (state, action) => ({
loading: {
...state.loading,
updatingProduct: false,
},
}),
[GET_TOTAL_PRICE_REQUEST]: (state, action) => ({
totalPrice: null,
loading: {
...state.loading,
totalPrice: true,
},
}),
[GET_TOTAL_PRICE_SUCCESS]: (state, {totalPrice}) => ({
totalPrice,
loading: {
...state.loading,
totalPrice: false,
}
}),
[EDITING_RETAIL_PRICE]: (state, {totalPrice}) => ({
totalPrice
}),
[RETAIL_PRICE_UPDATED]: (state, {totalPrice}) => ({
totalPrice,
addRetailPrice: false
}),
[GET_TOTAL_PRICE_FAILURE]: (state, {error}) => ({
error,
loading: {
...state.loading,
totalPrice: false,
},
}),
[CHANGE_MARKUP_FACTOR]: (state, {markupFactor}) => ({
markupFactor,
}),
[CHANGE_SELECTED_PRODUCT]: (state, {selectedProduct}) => ({
selectedProduct,
}),
[CHANGE_RETAIL_PRICE]: (state) => ({
addRetailPrice: true,
}),
[ADD_CATEGORIES]: (state,{ categories }) => ({
categories
}),
[CLEAR]: (state, action) => RESET_STORE,
})
In your every reducer you have to return previous state and a new state.
[ACTION]:(state, ...) => ({
...state, <------ previouse state
... < ------- your new data (loading, others)
}),
Right now you don't return previous store state, instead you overwrite the store with only new data
[GET_PRODUCT_REQUEST]: (state, {params}) => ({
...state,
...
}),
[GET_PRODUCT_SUCCESS]: (state, {res: {results}}) => ({
...state,
...
products: results,
loading: false,
}),
Related
I have a react component that fetches from API with createAsyncThunk and cannot understand a behaviour that happens on Mount in this part:
if(isLoading===true) return <div>Loading...</div>
if(failedToLoad===true) return <div>Error loading feed</div>
if(feedResponse) return(
<div className={styles.feed}>
{feedResponse.map(({id}) => {
return(
<Link to={`/thread=${id}`} key={id}>
<Thread key={id} id={id}/>
</Link>
)
})}
</div>
)
If I remove if(feedResponse) next to the return(...) the component will crash because it will try to render before feedResponse status has data. Why isn't that covered by the first two IFs?
if(isLoading===true) return <div>Loading...</div>
if(failedToLoad===true) return <div>Error loading feed</div>
It is my understanding that there is no scenario where we have an inLoading = false & feedResponse = null
Here is the complete code if needed:
Feed.js
export const Feed = () => {
const dispatch = useDispatch()
const feedResponse = useSelector(selectFeedResponse)
const isLoading = useSelector(isLoadingFeed)
const failedToLoad = useSelector(failedToLoadFeed)
const location = useLocation()
useUpdateEffect(() => {
dispatch(SearchThunk(searchTerm))
}, searchTerm)
useUpdateEffect(() => {
dispatch(homeThunk(location.pathname+'.json'))
}, location)
if(isLoading===true) return <div>Loading...</div>
if(failedToLoad===true) return <div>Error loading feed</div>
if(feedResponse) return(
<div className={styles.feed}>
{feedResponse.map(({id}) => {
return(
<Link to={`/thread=${id}`} key={id}>
<Thread key={id} id={id}/>
</Link>
)
})}
</div>
)
}
feedSlice.js
export const homeThunk = createAsyncThunk(
'feed/homeThunk',
async (homePath) => {
const response = await fetch(`https://www.reddit.com${homePath}`)
const json = await response.json()
const threads = json.data.children.map(thread => {
return {
id: thread.data.id,
subreddit: thread.data.subreddit,
title: thread.data.title,
author: thread.data.author,
thumbnail: thread.data.thumbnail,
created: thread.data.created,
score: thread.data.score,
num_comments: thread.data.num_comments
}
})
return threads
}
)
export const feedSlice = createSlice({
name: 'feed',
initialState: {
feedResponse: '',
isLoadingFeed: false,
failedToLoadFeed: false
},
extraReducers: (builder) => {
builder
.addCase(homeThunk.pending, (state) => {
state.isLoadingFeed = true
state.failedToLoadFeed = false
})
.addCase(homeThunk.fulfilled, (state, action) => {
state.isLoadingFeed = false
state.failedToLoadFeed = false
state.feedResponse = action.payload
})
.addCase(homeThunk.rejected, (state) => {
state.isLoadingFeed = false
state.failedToLoadFeed = true
})
}
})
export const selectFeedResponse = state => state.feed.feedResponse
export const isLoadingFeed = state => state.feed.isLoadingFeed
export const failedToLoadFeed = state => state.feed.failedToLoadFeed
export default feedSlice.reducer
feedUtilities.js
import { useEffect, useRef } from "react";
export const useUpdateEffect = (effect, deps = []) => {
const isFirstMount = useRef(true);
useEffect(() => {
if(!isFirstMount.current) effect()
else isFirstMount.current = false
}, [deps]);
}
I am writing a Jest/Testing Library unit test.
In test, I wrap my component in AuthProvider component:
const handlers: Record<string, (state: State, action: Action) => State> = {
INITIALIZE: (state: State, action: InitializeAction): State => {
const { isAuthenticated, permissions, user } = action.payload;
return {
...state,
isAuthenticated,
isInitialized: true,
permissions,
user,
};
},
LOGIN: (state: State): State => {
return {
...state,
isAuthenticated: true,
};
},
LOGOUT: (state: State): State => ({
...state,
isAuthenticated: false,
permissions: [],
}),
};
const reducer = (state: State, action: Action): State =>
handlers[action.type] ? handlers[action.type](state, action) : state;
const AuthContext = createContext<AuthContextValue>({
...initialState,
platform: 'JWT',
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
});
export const AuthProvider: FC<AuthProviderProps> = (props) => {
const { children } = props;
const [state, dispatch] = useReducer(reducer, initialState);
const router = useRouter();
const reduxDispatch = useDispatch();
useEffect(() => {
const initialize = async (): Promise<void> => {
try {
if (router.isReady) {
const { token, permissions, user, companyId } = router.query;
// TODO: Move all of this stuff from query and localstorage into session
const accessToken =
(token as string) || window.localStorage.getItem('accessToken');
const permsStorage = window.localStorage.getItem('perms');
const perms = (permissions as string) || permsStorage;
const userStorage = window.localStorage.getItem('user');
const selectedCompanyId =
(companyId as string) || window.localStorage.getItem('companyId');
const authUser = (user as string) || userStorage;
if (accessToken && perms) {
setSession(accessToken, perms, authUser);
try {
// check if user is admin by this perm, probably want to add a flag later
if (perms.includes('create:calcs')) {
if (!selectedCompanyId) {
const response = await reduxDispatch(getAllCompanies());
const companyId = response.payload[0].id;
reduxDispatch(companyActions.selectCompany(companyId));
reduxDispatch(getCurrentCompany({ companyId }));
} else {
reduxDispatch(
companyActions.selectCompany(selectedCompanyId),
);
await reduxDispatch(
getCurrentCompany({ companyId: selectedCompanyId }),
);
}
} else {
reduxDispatch(companyActions.selectCompany(selectedCompanyId));
await reduxDispatch(
getCurrentCompany({ companyId: selectedCompanyId }),
);
}
} catch (e) {
console.warn(e);
} finally {
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: true,
permissions: JSON.parse(perms),
user: JSON.parse(authUser),
},
});
}
if (token || permissions) {
router.replace(router.pathname, undefined, { shallow: true });
}
} else {
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: false,
permissions: [],
user: undefined,
},
});
setSession(undefined);
if (router.pathname !== '/client-landing') {
router.push('/login');
}
}
}
} catch (err) {
console.error(err);
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: false,
permissions: [],
user: undefined,
},
});
//router.push('/login');
}
};
initialize();
}, [router.isReady]);
const login = useCallback(async (): Promise<void> => {
const response = await axios.get('/auth/sign-in-with-intuit');
window.location = response.data;
}, []);
const logout = useCallback(async (): Promise<void> => {
const token = localStorage.getItem('accessToken');
// only logout if already logged in
if (token) {
dispatch({ type: 'LOGOUT' });
}
setSession(null);
router.push('/login');
}, [dispatch, router]);
return (
<AuthContext.Provider
value={{
...state,
platform: 'JWT',
login,
logout,
}}
>
{state.isInitialized && children}
</AuthContext.Provider>
);
};
AuthProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default AuthContext;
When I render the component wrapped with AuthProvider, it says
TypeError: Cannot read property 'isReady' of null
217 |
218 | initialize();
> 219 | }, [router.isReady]);
How can I make the router exist?
This is how I am rendering in Jest:
render(
<HelmetProvider>
<Provider store={mockStore(initState)}>
<AuthProvider>
<BenchmarksPage />
</AuthProvider>
,
</Provider>
</HelmetProvider>,
);
Note that I do not have to check if router works. I just have to make it run so that I can unit test the UI, no implementation details.
I have a react application and I try to implement redux within this application.
So my files look like this:
reducer.js
import {
CHANGE_SEARCH_FIELD,
REQUEST_ROBOTS_FAILED,
REQUEST_ROBOTS_PENDING,
REQUEST_ROBOTS_SUCCESS
} from './constants';
const initialState = {
searchField: ''
}
export const searchRobots = (state = initialState, action = {}) => {
console.log(action.type);
switch (action.type) {
case CHANGE_SEARCH_FIELD:
return Object.assign({}, state, { searchField: action.payload });
default:
return state;
}
}
const initialStateRobots = {
robots: [],
isPending: true
}
export const requestRobots = (state = initialStateRobots, action = {}) => {
switch (action.type) {
case REQUEST_ROBOTS_PENDING:
return Object.assign({}, state, { isPending: true })
case REQUEST_ROBOTS_SUCCESS:
return Object.assign({}, state, { robots: action.payload, isPending: false })
case REQUEST_ROBOTS_FAILED:
return Object.assign({}, state, { error: action.payload })
default:
return state
}
}
actions.js
import {
CHANGE_SEARCH_FIELD,
REQUEST_ROBOTS_FAILED,
REQUEST_ROBOTS_PENDING,
REQUEST_ROBOTS_SUCCESS
} from './constants'
export const setSearchField = (text) => ({
type: CHANGE_SEARCH_FIELD,
payload: text
});
export const requestRobots = () => (dispatch) => {
dispatch({ type: REQUEST_ROBOTS_PENDING })
fetch('https://jsonplaceholder.typicode.com/users')
.then(data => dispatch({ type: REQUEST_ROBOTS_SUCCESS, payload: data }))
.catch(error => dispatch({ type: REQUEST_ROBOTS_FAILED, payload: error }))
}
index.js:
const logger = createLogger();
const rootReducer = combineReducers({searchRobots, requestRobots});
const store = createStore(rootReducer, applyMiddleware( thunkmiddleware,logger));
ReactDOM.render(<Provider store={store}>
<App />
</Provider>, document.getElementById('root'));
registerServiceWorker();
and app.js:
const mapStateToProps = state => {
return {
searchField: state.searchField,
robots: state.requestRobots.robots,
isPending: state.requestRobots.isPending
}
};
const mapDispatchToProps = (dispatch) => {
return {
onSearchChange: (event) => dispatch(setSearchField(event.target.value)),
onRequestRobots: () => dispatch(requestRobots())
}
}
function App(props) {
useEffect(() => {
props.onRequestRobots();
console.log(props);
}, []);
const filteredRobots = robots.filter(robot => {
return robot.name.toLowerCase().includes(searchField.toLowerCase());
});
return !robots.length ?
<h1>Loading</h1> :
(
<div className='tc'>
<h1 className='f1'>RoboFriends</h1>
<SearchBox searchChange={props.onSearchChange} />
<Scroll>
<CardList robots={filteredRobots} />
</Scroll>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
But I get this errror:
ReferenceError: robots is not defined
App
E:/Mijn Documents/UDEMY/REACT/robofriends-master/src/containers/App.js:40
37 |
38 |
39 | //const { robots, searchField, onSearchChange, isPending } = props;
> 40 | const filteredRobots = robots.filter(robot => {
| ^ 41 | return robot.name.toLowerCase().includes(searchField.toLowerCase());
42 | });
43 | return !robots.length ?
my question: what I have to change?
Thank you
You should get robots from props like this:
const filteredRobots = props.robots.filter(robot => {
return robot.name.toLowerCase().includes(searchField.toLowerCase());
});
I want to access Redux Store State in presentational component, but the state is undefined.
I made Container Component, Presentational Component, and Redux Store.
So I tried to access user in presentational component but user is undefined.
I use Redux-Thunk when create Redux Store for asynchronous API request.
The doubful part is that Rdux Store State has a value but i can't access the state user in presentaional component
// headerState.js
import { handleActions } from 'redux-actions';
import * as api from '../lib/api';
const USER_STATE = 'headerState/USER_STATE';
const USER_STATE_SUCCESS = 'headerState/USER_STATE_SUCCESS';
const USER_STATE_FAILURE = 'headerState/USER_STATE_FAILURE';
const USER_LOGOUT = 'headerState/USER_LOGOUT';
const USER_LOGOUT_SUCCESS = 'headerState/USER_LOGOUT_SUCCESS';
const USER_LOGOUT_FAILURE = 'headerState/USER_LOGOUT_FAILURE';
export const getUserInfo = () => async (dispatch) => {
dispatch({ type: USER_STATE });
try {
const response = await api.getUser();
dispatch({
type: USER_STATE_SUCCESS,
payload: response.data,
});
} catch (error) {
dispatch({
type: USER_STATE_FAILURE,
payload: error,
error: true,
});
throw error;
}
};
export const userLogout = () => async (dispatch) => {
dispatch({ type: USER_LOGOUT });
try {
const response = await api.logout();
dispatch({
type: USER_LOGOUT_SUCCESS,
payload: response.data,
});
} catch (error) {
dispatch({
type: USER_LOGOUT_FAILURE,
payload: error,
error: true,
});
throw error;
}
};
const initialState = {
user: null,
loading: {
isProcessing: false,
},
};
const headerState = handleActions(
{
[USER_STATE]: (state) => ({
...state,
loading: {
...state.loading,
isProcessing: true,
},
}),
[USER_STATE_SUCCESS]: (state, action) => ({
...state,
user: action.payload,
loading: {
...state.loading,
isProcessing: false,
},
}),
[USER_STATE_FAILURE]: (state) => ({
...state,
loading: {
...state.loading,
isProcessing: false,
},
}),
[USER_LOGOUT]: (state) => ({
...state,
loading: {
...state.loading,
isProcessing: true,
},
}),
[USER_LOGOUT_SUCCESS]: (state, action) => ({
...state,
user: action.payload,
loading: {
...state.loading,
isProcessing: false,
},
}),
[USER_LOGOUT_FAILURE]: (state) => ({
...state,
loading: {
...state.loading,
isProcessing: false,
},
}),
},
initialState,
);
export default headerState;
// HeaderContainer.js
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import Header from './common/Header';
import { getUserInfo, userLogout } from '../modules/headerState';
const HeaderContainer = ({ user, getUserInfo, userLogout }) => {
useEffect(() => {
getUserInfo();
}, [getUserInfo]);
return <Header user={user} logout={userLogout} />;
};
const mapStateToProps = (state) => {
return {
user: state.user,
loading: state.loading,
};
};
export default connect(mapStateToProps, {
getUserInfo,
userLogout,
})(HeaderContainer);
// Header.js
(...)
const Header = ({ user, logout }) => { ** // user is undefined **
return (
<>
<TopNav>
<Link to="/" style={TitleLinkStyle}>
<NavTitle>
<img
src={logo}
width="50"
height="50"
color="white"
style={{ paddingRight: '25px', alignSelf: 'center' }}
alt="logo"
/>
Service Title
</NavTitle>
</Link>
<div style={{ display: 'flex', flex: 4 }} />
<SubNav>
<Link to="/home" style={{ textDecoration: 'none', color: 'white' }}>
<HomeDiv>Home</HomeDiv>
</Link>
{!user.snsId && (
<Link
to="/login"
style={{ textDecoration: 'none', color: 'white' }}
>
<LoginDiv>Login</LoginDiv>
</Link>
)}
{user.snsId && (
<LoginDiv onClick={logout} style={{ cursor: 'pointer' }}>
Logout
</LoginDiv>
)}
</SubNav>
</TopNav>
</>
);
};
export default Header;
There are two potential problems.
First, the initial value of user is null until USER_STATE_SUCCESS is dispatched. So trying to access user.snsId in your presentation component will cause a type error:
Cannot read property 'snsId' of undefined
You can use optional chaining to attempt to access snsId and simplify your logic.
// Header.js
const Header = ({ user }) => {
return user?.snsId ? <div>Logout</div> : <div>Login</div>;
};
The second issue potentially lies in how you are creating and accessing your store. I have assumed that you are doing something like this:
const reducers = combineReducers({
header: headerReducer
});
const store = createStore(reducers);
When using mapStateToProps, your container component needs to access that state slice by the same key with which it was created - header.
// HeaderContainer.js
const mapStateToProps = (state) => {
return {
user: state.header.user, // state.user is undefined
loading: state.header.loading // state.loading is undefined
};
};
I'm pretty new to javascript, and I've been following a tutorial to create an e-commerce website. For some reason I am now getting a TypeError: Cannot read property 'category' of undefined error in reference to:
export const listProducts = ({category = '' }) => async (dispatch)
part of my productAction.js document. I've included the rest of my productAction.js document below, as well as the other important documents. I would really appreciate any help or guidance on this issue.
productAction.js
import Axios from 'axios';
import {
PRODUCT_CREATE_FAIL,
PRODUCT_CREATE_REQUEST,
PRODUCT_CREATE_SUCCESS,
PRODUCT_DETAILS_FAIL,
PRODUCT_DETAILS_REQUEST,
PRODUCT_DETAILS_SUCCESS,
PRODUCT_LIST_FAIL,
PRODUCT_LIST_REQUEST,
PRODUCT_LIST_SUCCESS,
PRODUCT_UPDATE_REQUEST,
PRODUCT_UPDATE_SUCCESS,
PRODUCT_UPDATE_FAIL,
PRODUCT_DELETE_REQUEST,
PRODUCT_DELETE_FAIL,
PRODUCT_DELETE_SUCCESS,
PRODUCT_REVIEW_CREATE_REQUEST,
PRODUCT_REVIEW_CREATE_SUCCESS,
PRODUCT_REVIEW_CREATE_FAIL,
PRODUCT_CATEGORY_LIST_SUCCESS,
PRODUCT_CATEGORY_LIST_REQUEST,
PRODUCT_CATEGORY_LIST_FAIL,
/*PRODUCT_SAVE_REQUEST,*/
} from '../constants/productConstants';
export const listProducts = ({name = '', category = '' }) => async (dispatch) => {
dispatch({
type: PRODUCT_LIST_REQUEST,
});
try {
const { data } = await Axios.get(`/api/products?name=${name}category=${category}`);
dispatch({ type: PRODUCT_LIST_SUCCESS, payload: data });
} catch (error) {
dispatch({ type: PRODUCT_LIST_FAIL, payload: error.message });
}
};
export const listProductCategories = () => async (dispatch) => {
dispatch({
type: PRODUCT_CATEGORY_LIST_REQUEST,
});
try {
const { data } = await Axios.get(`/api/products/categories`);
dispatch({ type: PRODUCT_CATEGORY_LIST_SUCCESS, payload: data });
} catch (error) {
dispatch({ type: PRODUCT_CATEGORY_LIST_FAIL, payload: error.message });
}
};
export const detailsProduct = (productId) => async (dispatch) => {
dispatch({ type: PRODUCT_DETAILS_REQUEST, payload: productId });
try {
const { data } = await Axios.get(`/api/products/${productId}`);
dispatch({ type: PRODUCT_DETAILS_SUCCESS, payload: data });
} catch (error) {
dispatch({
type: PRODUCT_DETAILS_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
export const createProduct = () => async (dispatch, getState) => {
dispatch({ type: PRODUCT_CREATE_REQUEST });
const {
userSignin: { userInfo },
} = getState();
try {
const { data } = await Axios.post(
'/api/products',
{},
{
headers: { Authorization: `Bearer ${userInfo.token}` },
}
);
dispatch({
type: PRODUCT_CREATE_SUCCESS,
payload: data.product,
});
} catch (error) {
const message =
error.response && error.response.data.message
? error.response.data.message
: error.message;
dispatch({ type: PRODUCT_CREATE_FAIL, payload: message });
}
};
export const updateProduct = (product) => async (dispatch, getState) => {
dispatch({ type: PRODUCT_UPDATE_REQUEST, payload: product });
const {
userSignin: { userInfo },
} = getState();
try {
const { data } = await Axios.put(`/api/products/${product._id}`, product, {
headers: { Authorization: `Bearer ${userInfo.token}` },
});
dispatch({ type: PRODUCT_UPDATE_SUCCESS, payload: data });
} catch (error) {
const message =
error.response && error.response.data.message
? error.response.data.message
: error.message;
dispatch({ type: PRODUCT_UPDATE_FAIL, error: message });
}
};
export const deleteProduct = (productId) => async (dispatch, getState) => {
dispatch({ type: PRODUCT_DELETE_REQUEST, payload: productId });
const {
userSignin: { userInfo },
} = getState();
try {
const {data} = Axios.delete(`/api/products/${productId}`, {
headers: { Authorization: `Bearer ${userInfo.token}` },
});
dispatch({ type: PRODUCT_DELETE_SUCCESS });
} catch (error) {
const message =
error.response && error.response.data.message
? error.response.data.message
: error.message;
dispatch({ type: PRODUCT_DELETE_FAIL, payload: message });
}
};
export const createReview = (productId, review) => async (
dispatch,
getState
) => {
dispatch({ type: PRODUCT_REVIEW_CREATE_REQUEST });
const {
userSignin: { userInfo },
} = getState();
try {
const { data } = await Axios.post(
`/api/products/${productId}/reviews`,
review,
{
headers: { Authorization: `Bearer ${userInfo.token}` },
}
);
dispatch({
type: PRODUCT_REVIEW_CREATE_SUCCESS,
payload: data.review,
});
} catch (error) {
const message =
error.response && error.response.data.message
? error.response.data.message
: error.message;
dispatch({ type: PRODUCT_REVIEW_CREATE_FAIL, payload: message });
}
};
productReducer.js
const {
PRODUCT_LIST_REQUEST,
PRODUCT_LIST_SUCCESS,
PRODUCT_LIST_FAIL,
PRODUCT_CATEGORY_LIST_REQUEST,
PRODUCT_CATEGORY_LIST_SUCCESS,
PRODUCT_CATEGORY_LIST_FAIL,
} = require('../constants/productConstants');
export const productListReducer = (
state = { loading: true, products: [] },
action
) => {
switch (action.type) {
case PRODUCT_LIST_REQUEST:
return { loading: true };
case PRODUCT_LIST_SUCCESS:
return { loading: false, products: action.payload };
case PRODUCT_LIST_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
export const productCategoryListReducer = (
state = { loading: true, products: [] },
action
) => {
switch (action.type) {
case PRODUCT_CATEGORY_LIST_REQUEST:
return { loading: true };
case PRODUCT_CATEGORY_LIST_SUCCESS:
return { loading: false, categories: action.payload };
case PRODUCT_CATEGORY_LIST_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
Backend
productModel.js
import mongoose from 'mongoose';
const reviewSchema = new mongoose.Schema(
{
name: { type: String, required: true },
comment: { type: String, required: true },
rating: { type: Number, required: true },
},
{
timestamps: true,
}
);
const productSchema = new mongoose.Schema(
{
name: { type: String, required: true, unique: true },
image: { type: String, required: true },
brand: { type: String, required: true },
category: { type: String, required: true },
description: { type: String, required: true },
price: { type: Number, required: true },
countInStock: { type: Number, required: true },
rating: { type: Number, required: true },
numReviews: { type: Number, required: true },
reviews: [reviewSchema],
},
{
timestamps: true,
}
);
const Product = mongoose.model('Product', productSchema);
export default Product;
productRouter.js
import express from 'express';
import expressAsyncHandler from 'express-async-handler';
import data from '../data.js';
import Product from '../models/productModel.js';
import { isAdmin, isAuth } from '../utils.js';
const productRouter = express.Router();
productRouter.get(
'/',
expressAsyncHandler(async (req, res) => {
const name = req.query.name || '';
const category = req.query.category || '';
const nameFilter = name ? { name: { $regex: name, $options: 'i' } } : {};
const categoryFilter = category ? { category } : {};
const products = await Product.find({
...nameFilter,
...categoryFilter,
}).res.send(products);
})
);
productRouter.get(
'/categories',
expressAsyncHandler(async (req, res) => {
const categories = await Product.find().distinct('category');
res.send(categories);
})
);
productRouter.get(
'/seed',
expressAsyncHandler(async (req, res) => {
// await Product.remove({});
const createdProducts = await Product.insertMany(data.products);
res.send({ createdProducts });
})
);
productRouter.get(
'/:id',
expressAsyncHandler(async (req, res) => {
const product = await Product.findById(req.params.id);
if (product) {
res.send(product);
} else {
res.status(404).send({ message: 'Product Not Found' });
}
})
);
productRouter.post(
'/',
isAuth,
isAdmin,
expressAsyncHandler(async (req, res) => {
const product = new Product({
name: 'sample name ' + Date.now(),
image: '/images/p1.jpg',
price: 0,
category: 1,
brand: 'sample brand',
countInStock: 0,
rating: 0,
numReviews: 0,
description: 'sample description',
});
const createdProduct = await product.save();
res.send({ message: 'Product Created', product: createdProduct });
})
);
productRouter.put(
'/:id',
isAuth,
isAdmin,
expressAsyncHandler(async (req, res) => {
const productId = req.params.id;
const product = await Product.findById(productId);
if (product) {
product.name = req.body.name;
product.price = req.body.price;
product.image = req.body.image;
product.category = req.body.category;
product.brand = req.body.brand;
product.countInStock = req.body.countInStock;
product.description = req.body.description;
const updatedProduct = await product.save();
res.send({ message: 'Product Updated', product: updatedProduct });
} else {
res.status(404).send({ message: 'Product Not Found' });
}
})
);
export default productRouter;
Update:
So, I think the issue was my listProducts constant. In my HomeScreen.js document (not previously included), I realized that I had forgot to add {} within dispatch(listProducts());
useEffect(() =>{
dispatch(listProducts({}));
}, [dispatch]);
My only issue is that while my page now loads, I now have a Request failed with status code 500 error.
I've included the rest of my HomeScreen.js document and my other documents that include listProducts. Any additional guidance would be greatly appreciated.
HomeScreen.js
import React, { useEffect } from 'react';
import Product from '../components/Product';
import LoadingBox from '../components/LoadingBox';
import MessageBox from '../components/MessageBox';
import { useDispatch, useSelector } from 'react-redux';
import { listProducts } from '../actions/productActions';
import { Link } from 'react-router-dom';
export default function HomeScreen() {
const dispatch = useDispatch();
const productList = useSelector((state) => state.productList);
const { loading, error, products } = productList;
useEffect(() =>{
dispatch(listProducts({}));
}, [dispatch]);
return (
<div>
{loading ? (
<LoadingBox></LoadingBox>
) : error ? (
<MessageBox variant="danger">{error}</MessageBox>
) : (
<>
{products.length === 0 && <MessageBox>No Product Found</MessageBox>}
<div className="row center">
{products.map((product) => (
<Product key={product._id} product={product}></Product>
))}
</div>
</>
)}
</div>
);
}
SearchScreen.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useParams } from 'react-router-dom';
import { listProducts } from '../actions/productActions';
import LoadingBox from '../components/LoadingBox';
import MessageBox from '../components/MessageBox';
import Product from '../components/Product';
export default function SearchScreen(props) {
const { name = 'all', category = 'all' } = useParams();
const dispatch = useDispatch();
const productList = useSelector((state) => state.productList);
const { loading, error, products } = productList;
const productCategoryList = useSelector((state) => state.productCategoryList);
const {
loading: loadingCategories,
error: errorCategories,
categories,
} = productCategoryList;
useEffect(() => {
dispatch(
listProducts({
name: name !== 'all' ? name : '',
category: category !== 'all' ? category : '',
})
);
}, [category, dispatch, name]);
const getFilterUrl = (filter) => {
const filterCategory = filter.category || category;
const filterName = filter.name || name;
return `/search/category/${filterCategory}/name/${filterName}`;
};
return (
<div>
<div className="row">
{loading ? (
<LoadingBox></LoadingBox>
) : error ? (
<MessageBox variant="danger">{error}</MessageBox>
) : (
<div>{products.length} Results</div>
)}
</div>
<div className="row top">
<div className="col-1">
<h3>Department</h3>
{loadingCategories ? (
<LoadingBox></LoadingBox>
) : errorCategories ? (
<MessageBox variant="danger">{errorCategories}</MessageBox>
) : (
<ul>
{categories.map((c) => (
<li key={c}>
<Link
className={c === category ? 'active' : ''}
to={getFilterUrl({ category: c })}
>
{c}
</Link>
</li>
))}
</ul>
)}
</div>
<div className="col-3">
{loading ? (
<LoadingBox></LoadingBox>
) : error ? (
<MessageBox variant="danger">{error}</MessageBox>
) : (
<>
{products.length === 0 && (
<MessageBox>No Product Found</MessageBox>
)}
<div className="row center">
{products.map((product) => (
<Product key={product._id} product={product}></Product>
))}
</div>
</>
)}
</div>
</div>
</div>
);
}
ProductListScreen.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
createProduct,
deleteProduct,
listProducts,
} from '../actions/productActions';
import LoadingBox from '../components/LoadingBox';
import MessageBox from '../components/MessageBox';
import {
PRODUCT_CREATE_RESET,
PRODUCT_DELETE_RESET,
} from '../constants/productConstants';
export default function ProductListScreen(props) {
const productList = useSelector((state) => state.productList);
const { loading, error, products } = productList;
const productCreate = useSelector((state) => state.productCreate);
const {
loading: loadingCreate,
error: errorCreate,
success: successCreate,
product: createdProduct,
} = productCreate;
const productDelete = useSelector((state) => state.productDelete);
const {
loading: loadingDelete,
error: errorDelete,
success: successDelete,
} = productDelete;
const dispatch = useDispatch();
useEffect(() => {
if (successCreate) {
dispatch({ type: PRODUCT_CREATE_RESET });
props.history.push(`/product/${createdProduct._id}/edit`);
}
if (successDelete) {
dispatch({ type: PRODUCT_DELETE_RESET });
}
dispatch(listProducts());
/// TODO: dispatch delete action
}, [createdProduct, dispatch, props.history, successCreate, successDelete]);
const deleteHandler = (product) => {
if (window.confirm('Are you sure to delete?')) {
dispatch(deleteProduct(product._id));
}
};
const createHandler = () => {
dispatch(createProduct());
};
return (
<div>
<div className="row">
<h1>Products</h1>
<button type="button" className="primary" onClick={createHandler}>
Create Product
</button>
</div>
{loadingDelete && <LoadingBox></LoadingBox>}
{errorDelete && <MessageBox variant="danger">{errorDelete}</MessageBox>}
{loadingCreate && <LoadingBox></LoadingBox>}
{errorCreate && <MessageBox variant="danger">{errorCreate}</MessageBox>}
{loading ? (
<LoadingBox></LoadingBox>
) : error ? (
<MessageBox variant="danger">{error}</MessageBox>
) : (
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>NAME</th>
<th>PRICE</th>
<th>CATEGORY</th>
<th>BRAND</th>
<th>ACTIONS</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr key={product._id}>
<td>{product._id}</td>
<td>{product.name}</td>
<td>{product.price}</td>
<td>{product.category}</td>
<td>{product.brand}</td>
<td>
<button
type="button"
className="small"
onClick={() =>
props.history.push(`/product/${product._id}/edit`)
}
>
Edit
</button>
<button
type="button"
className="small"
onClick={() => deleteHandler(product)}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}
Based on the error it seems you may be calling listProducts and not passing any argument. If this is expected then you can provide an initial value for the argument to destructure category from.
export const listProducts = ({ category = '' } = {}) => async (dispatch) => {
dispatch({
type: PRODUCT_LIST_REQUEST,
});
try {
const { data } = await Axios.get(`/api/products?category=${category}`);
dispatch({ type: PRODUCT_LIST_SUCCESS, payload: data });
} catch (error) {
dispatch({ type: PRODUCT_LIST_FAIL, payload: error.message });
}
};
SO the most plausible answer according to me is (based on the error provided),
The error is causing because you are not passing anything. Due to which the javascript is trying to spread nothing and giving you the error "Cannot read property 'category' of undefined".
I know this is a bit confusing let me explain it with an example
let's say you have a function
const test = ({context = ""}) => {
console.log(context)
}
and if you call the test function without passing an object like below
test();
this will produce the error "Cannot read property 'category' of undefined" as the function definition is trying to spread an object and get a context property out of that object but you are passing nothing.
instead, if you call the function
test({});
or
test({context=1});
this will not cause you any error as function definition will get an object to spread.
for more details on object spread syntax, you can refer MDN docs here
Hope its helpful.