I tried to add a recipe to favourites using Redux with React Native.
If I use the state in the favicon.js this works fine but does not work with Redux.
This is my favicon.js
import React, {useState} from 'react'
import { FontAwesome } from '#expo/vector-icons'
import { View, TouchableOpacity,StyleSheet, Text, Alert } from 'react-native'
import { connect } from 'react-redux'
import { toggleFavId, isFavourite } from '../actions'
const FavIcon = ({recipeid, toggle, isFav, text}) => {
// const [isFavNew, setFavNew] = useState(false)
// const toggleFav = (recipeid) =>{
// setFavNew(!isFavNew)
// console.log(`Entered toggle ${recipeid} isfavnew = ${isFavNew}`)
// }
return (
<View>
<TouchableOpacity style={styles.favItem} onPress={() => {toggle(recipeid)}}>
<FontAwesome name={isFav == true ? 'heart' : 'heart-o'} size={40} color='red' />
<Text> {text}</Text>
</TouchableOpacity>
</View>
)
}
const mapStateToProps = (state) =>({
favids: state.favids,
})
const mapDispatchToProps = (dispatch) => ({
toggle: (recipeid) => dispatch(toggleFavId(recipeid)),
isFav: (recipeid) => dispatch(isFavourite(recipeid)),
})
export default connect(mapStateToProps,mapDispatchToProps)(FavIcon)
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
backgroundColor: '#fff',
alignItems: 'flex-start',
justifyContent: 'center',
},
favItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
marginTop:10,
paddingTop:15,
paddingBottom:15,
marginLeft:30,
marginRight:30,
backgroundColor:'#00BCD4',
borderRadius:10,
borderWidth: 1,
borderColor: '#fff',
height: 75,
width: 300,
}
});
This if the reducer....the code enters the TOGGLE switch but the state is not updated with the new values.
import {TOGGLE, IS_FAVOURITE} from '../actions'
export const favids = (state=[], action) => {
const {type, recipeid} = action
const newRecipe = {
recipeid: recipeid,
is_fav: true,
}
switch (type) {
case TOGGLE: {
console.log(`Toggle State = ${JSON.stringify(state)}`)
console.log(`NEW REcipe ${JSON.stringify(newRecipe)} `)
// console.log(`Toggle State after adding = ${JSON.stringify(state)}`)
// return state.map(item=>{
// state.recipeid === recipeid ?
// {...item, newRecipe} : {...item,newRecipe}
// })
return {...state, newRecipe}
}
case IS_FAVOURITE: {
console.log(`Is Favourite state =${JSON.stringify(state)}`)
return state.map(item=>{
item.recipeid === recipeid ?
{...item, is_fav: !item.is_fav} : item
})
}
default:
return state
}
}
I've tried different methods of updating the state (see below) but the state is not updated.
I've commented out the older code which was not working
// return state.map(item=>{
// state.recipeid === recipeid ?
// {...item, newRecipe} : {...item,newRecipe}
// })
return {...state, newRecipe}
Any help would be appreciated.
This is the index.js file from the '../actions' folder
export const TOGGLE = 'TOGGLE'
export const toggleFavId = id => ({
type: 'TOGGLE',
recipeid: id
})
export const IS_FAVOURITE = 'IS_FAVOURITE'
export const isFavourite = id => (
{
type: 'IS_FAVOURITE',
recipeid: id
}
)
export const TOGGLE_FAV = 'TOGGLE_FAV'
export const toggleFav = id => ({
type: 'TOGGLE_FAV',
recipeid: id,
})
this is the VisibleFavicon.js file
import {connect} from 'react-redux'
import Favicon from '../components/favicon'
const mapStateToProps = state =>({
favids: state
})
const mapDispatchToProps = dispatch => ({
toggleFavIds: id => dispatch ({
type: 'TOGGLE',
id
}),
isFavourite : id => dispatch ({
type: 'IS_FAVOURITE',
id
}),
})
export default connect(mapStateToProps, mapDispatchToProps)(Favicon)
Try to get whole state of redux instead of state.favids and wherever you want to use that state you can access it like props.state.favids :
const mapStateToProps = (state) =>({
favids: state,
})
Heres the solution to the issue, this is the code for the reducer.
import { TOGGLE, IS_FAVOURITE } from '../actions'
export const favids = (state = [], action) => {
const { type, recipeid } = action
const newRecipe = {
recipeid,
is_fav: true,
}
switch (type) {
case TOGGLE: {
return state.findIndex(recipe => recipe.recipeid === recipeid) === -1 ?
[...state, newRecipe] :
state.map(recipe =>
recipe.recipeid === recipeid ?
{ ...recipe, is_fav: !recipe.is_fav } : recipe
)
}
default:
return state
}
}
Related
I've been driving myself insane trying to figure out why this React component won't render. After checking in a local environment it works fine, but I need this to work on CodePen.io to turn it in for a FreeCodeCamp Challenge. Any help would be greatly appreciated.
I've tried putting in an 'unpkg' source, adding it to the html in script tags (the React packages) and using the integrated npm tool in the codepen.io js tab in the settings. None of this worked and I think the code should work.
import React from 'http://esm.sh/react#18.2.0'
import ReactDOM from 'http://esm.sh/react-dom#18.2.0'
import * as redux from "http://cdn.skypack.dev/redux#4.2.0";
import reduxThunk from "http://cdn.skypack.dev/redux-thunk#2.4.2";
import { Provider, connect } from "http://cdn.skypack.dev/react-redux#8.0.5";
// Initial State
const initialState = {
quote: "Let's go",
author: "Bob J. Baker",
backgroundColor: "#f5f5f5"
};
// Actions: Types
const CHANGE_ALL = "CHANGE_ALL";
// Action: Functions
const changeAll = () => {
return async (dispatch) => {
try {
dispatch({ type: CHANGE_ALL });
const response = await fetch("https://quotes.rest/quote/random");
const data = await response.json();
const color = getRandomColor();
dispatch({
type: CHANGE_ALL,
quote: data.contents.quotes[0].quote,
author: data.contents.quotes[0].author,
backgroundColor: color
});
} catch (error) {
console.log(error);
}
};
};
const getRandomColor = () => {
const randomColor = "#" + Math.random().toString(16).substr(-6);
return randomColor;
};
// Reducer
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case CHANGE_ALL:
return {
...state,
quoteAndAuthor: { quote: action.quote, author: action.author },
backgroundColor: action.backgroundColor
};
default:
return state;
}
};
// Store
const store = redux.createStore(rootReducer, redux.applyMiddleware(reduxThunk));
//**********Component://**********
class RandomQuoteComponent extends React.Component {
// state & props
constructor(props) {
super(props);
}
componentDidMount() {
// running the get all action
this.props.changeAll();
}
render() {
return (
<div
id="main-wrapper"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
width: "100%",
backgroundColor: this.props.backgroundColor
}}
>
<div id="quote-box">
<h2 id="text">{this.props.quote}</h2>
<h6 id="author">{this.props.author}</h6>
<button id="new-quote">New Quote</button>
<a href="#" id="tweet-quote">
<i className="fab fa-twitter"></i>
</a>
</div>
</div>
);
}
}
////**********END OF COMPONENT//**********
// Mapping for state and dispatch actions to props
const mapStateToProps = (state) => {
return {
backgroundColor: state.backgroundColor,
quote: state.quoteAndAuthor.quote,
author: state.quoteAndAuthor.author
};
};
const mapDispatchToProps = (dispatch) => {
return {
changeAll: () => dispatch(changeAll())
};
};
const ConnectedRandomQuoteComponent = connect(
mapStateToProps,
mapDispatchToProps
)(RandomQuoteComponent);
ReactDOM.render(
<Provider store={store}>
<ConnectedRandomQuoteComponent />
</Provider>,
document.getElementById("root")
);
I`m trying to implement a switch that can be switched on and off and according to the state a different image and text is shown. With the help of a tutorial I did the following:
First I created store.js:
import { configureStore } from '#reduxjs/toolkit';
import switchReducer from './switch';
export const store = configureStore({
reducer: {
switchVal: switchReducer
}
});
Then I created switch.js
import { createSlice } from '#reduxjs/toolkit';
const switchSlice = createSlice({
name: 'alarm',
initialState: {
active: true
},
reducers: {
toggleSwitch: (state) => {
state.active = !state.active;
},
}
});
export const toggleSwitch = switchSlice.actions.toggleSwitch;
export default switchSlice.reducer;
In App.js I imported {store} and wrapped around my BottomTab Navigator.
Now the switch is in Alarm.js which looks like this:
import React from 'react';
import { StyleSheet, Text, View, Image, ScrollView, Pressable, Switch } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { globalStyles } from '../components/globalStyles';
import { toggleSwitch } from '../components/switch';
function Alarm({navigation}) {
const switchValue = useSelector((state) => state.switchValue);
const dispatch = useDispatch();
const alarmEnabled = require('../images/Alarm_enabled.png');
const alarmDisabled = require('../images/Alarm_disabled.png');
function toggleSwitchHandler() {
if (switchValue == true) {
dispatch(toggleSwitch(value));
} else {
dispatch(toggleSwitch(value));
}
}
return (
<ScrollView>
<View style={globalStyles.containerBodyAlarm}>
<View style={globalStyles.containerMainAlarm}>
<View style={globalStyles.containerImageAlarm}>
<Image style={{ width: 130, height: 130, resizeMode: 'contain', marginTop: 20, marginBottom: 10}} source={switchValue ? alarmDisabled : alarmEnabled } />
</View>
<View style={globalStyles.containerButtonAlarm}>
<Text style={globalStyles.textButtonAlarm}>{switchValue ? 'Alarm is deactivated' : 'Alarm is activated'}</Text>
<Switch
trackColor={{false: '#919190', true: '#000099'}}
thumbColor={'#5E5E5D'}
value={switchValue}
onValueChange= {toggleSwitchHandler}
/>
<Text style={globalStyles.textButtonAlarm2}>{switchValue ? 'activate' : 'deactivate'}</Text>
</View>
</View>
</View>
</ScrollView>
);
}
export default Alarm;
const styles = StyleSheet.create({
pressed: {
opacity: 0.7,
},
Unfortunately it's not working. What am I doing wrong? I'm sorry for the bad coding, I'm not good at it.
Your initial state looks like:
initialState: {
active: true
},
However, you are getting the value by doing:
const switchValue = useSelector((state) => state.switchValue);
That does not exist, it needs to be:
const switchValue = useSelector((state) => state.active);
function toggleSwitchHandler() {
if (switchValue == true) {
dispatch(toggleSwitch(value));
} else {
dispatch(toggleSwitch(value));
}
}
The if here has no use since both callers have the same props.
Also, value does not exist, since you're flipping the boolean inside the reducer, no need for the param.
Change it to:
function toggleSwitchHandler() {
dispatch(toggleSwitch());
}
To invert the current boolean
you can make 2 reducers, one with value true the other with value false
import { createSlice } from '#reduxjs/toolkit';
const initialStateValue = {
active: true
}
const switchSlice = createSlice({
name: 'alarm',
initialState: initialStateValue,
reducers: {
toggleOn: (state = initialStateValue) => {
state.active = true;
},
toggleOff: (state = initialStateValue) => {
state.active = false;
}
}
});
export const {toggleOn, toggleOff} = switchSlice.actions;
export default switchSlice.reducer;
get the value of toggle
const switchValue = useSelector((state) => state.alarm.active);
use it
function toggleSwitchHandler() {
if (switchValue == true) {
dispatch(toggleOff());
} else {
dispatch(toggleOn());
}
}
setLogin: (state, action) => {
const toggle = (input) => !input;
state.active = toggle(state.active);
},
I was having the same issue and came up with above solution. :)
I've been going at it for 2 days and cannot figure it out :(
I have a tree-like conversation as you can see in the screenshot.
When a person types something in an empty input field, the Message is then added to the reducer array "conversationsData.messages". When that happens, the Replies component of each Message is listening to changes to only the ~3 replies of that message. If the replies change, then the Replies should rerender. Buuut... The problem is that every single Reply component, and thus every single message is being re-rendered which is causing lag.
Can you please help me get the memo to work properly?
ConversationManager/Conversation/Message.tsx
import React, { FunctionComponent, ReactElement, useRef, useState, useEffect, useMemo } from 'react'
import { useDispatch, useStore, useSelector } from 'react-redux'
import Autosuggest, { OnSuggestionSelected, ChangeEvent } from 'react-autosuggest'
import colors from '#common/colors'
import { updateMessage, removeMessage } from '#reducers/conversationsData'
import usePrevious from 'react-hooks-use-previous'
import MessageInterface, { MessageInitState } from '#interfaces/message'
import { RootState } from '#reducers/rootReducer'
import NewReply from './NewReply'
import { StyleSheet } from '#interfaces/common'
interface IMessageProps {
origMessage: MessageInterface,
isSubClone: boolean,
firstRender: boolean, // It's firstRender=true if we're rendering the message for the first time, "false" if it's a dynamic render
isStarter?: boolean
}
const MessageFunc = ({ origMessage, isSubClone, firstRender }: IMessageProps): ReactElement | null => {
if(!origMessage.id){
return null
}
const dispatch = useDispatch()
const store = useStore()
const state: RootState = store.getState()
const [inputSuggestions, setInputSuggestions] = useState<MessageInterface[]>([])
const [inputWidth, setInputWidth] = useState(0)
const $invisibleInput = useRef<HTMLInputElement>(null)
const isFirstRun = useRef(true)
const [localMessage, setLocalMessage] = useState<MessageInterface>(MessageInitState)
const previousLocalMessage = usePrevious<MessageInterface>(localMessage, MessageInitState)
useEffect(() => {
isFirstRun.current = true
setLocalMessage(origMessage)
}, [origMessage])
useEffect(() => {
if(!localMessage.id) return
if(isFirstRun.current == true){
setupInputWidth()
isFirstRun.current = false
}
if(previousLocalMessage.text != localMessage.text){
setupInputWidth()
}
if(previousLocalMessage.cloneId != localMessage.cloneId){
setupIfMessageClone()
}
}, [localMessage])
const characterMessages = state.conversationsData.messages.filter((m) => {
return m.characterId == origMessage.characterId
})
const parent: MessageInterface = characterMessages.find((m) => {
return m.id == origMessage.parentId
}) || MessageInitState
const setupIfMessageClone = () => { // This function is only relevant if this message is a clone of another one
if(!localMessage.cloneId) return
const cloneOf = characterMessages.find((m) => {
return m.id == localMessage.cloneId
}) || MessageInitState
setLocalMessage({
...localMessage,
text: cloneOf.text
})
}
const setupInputWidth = () => {
let width = $invisibleInput.current ? $invisibleInput.current.offsetWidth : 0
width = width + 30 // Let's make the input width a bit bigger
setInputWidth(width)
}
const _onFocus = () => {
// if(!localMessage.text){ // No message text, create a new one
// dispatch(updateMessage(localMessage))
// }
}
const _onBlur = () => {
if(localMessage.text){
dispatch(updateMessage(localMessage))
}
// No message, delete it from reducer
else {
dispatch(removeMessage(localMessage))
}
}
const _onChange = (event: React.FormEvent, { newValue }: ChangeEvent): void => {
setLocalMessage({
...localMessage,
cloneId: '',
text: newValue
})
}
const _suggestionSelected: OnSuggestionSelected<MessageInterface> = (event, { suggestion }) => {
setLocalMessage({
...localMessage,
cloneId: suggestion.id
})
}
const getSuggestions = (value: string): MessageInterface[] => {
const inputVal = value.trim().toLowerCase()
const inputLen = inputVal.length
return inputLen === 0 ? [] : characterMessages.filter(message =>
message.text.toLowerCase().slice(0, inputLen) === inputVal
)
}
if(!localMessage.id){
return null
}
else {
return (
<>
<li>
<div>
<Autosuggest
suggestions={inputSuggestions}
onSuggestionsFetchRequested={({ value }) => setInputSuggestions(getSuggestions(value))}
onSuggestionsClearRequested={() => setInputSuggestions([])}
getSuggestionValue={(suggestion) => suggestion.text}
onSuggestionSelected={_suggestionSelected}
renderSuggestion={(suggestion) => (
<div>
{suggestion.text}
</div>
)}
theme={{ ...autoSuggestTheme, input: {
...styles.input,
width: inputWidth,
borderBottomColor: localMessage.cloneId ? colors.purple : 'default',
borderBottomWidth: localMessage.cloneId ? 2 : 1
} }}
inputProps={{
value: localMessage.text,
onChange: _onChange,
onBlur: _onBlur,
onFocus: _onFocus,
className: 'form-control',
disabled: isSubClone
}}
/>
<span style={styles.invisibleSpan} ref={$invisibleInput}>{localMessage.text}</span>
</div>
<ul className="layer">
<Replies parentMessage={localMessage} isSubClone={isSubClone} />
</ul>
</li>
</>
)
}
}
const Message = React.memo(MessageFunc)
// const Message = MessageFunc
interface IRepliesProps {
parentMessage: MessageInterface,
isSubClone: boolean
}
const RepliesFunc: FunctionComponent<IRepliesProps> = ({
parentMessage, isSubClone
}: IRepliesProps): ReactElement | null => {
const previousParentMessage = usePrevious<MessageInterface>(parentMessage, MessageInitState)
const isFirstRun = useRef(true)
const replies: MessageInterface[] = useSelector((state: RootState) => state.conversationsData.messages.filter((m) => {
// If parent is regular message
if(!parentMessage.cloneId){
return m.parentId == parentMessage.id && m.characterId == parentMessage.characterId
}
// If parent is a clone, then replies need to come from the main clone
// else {
// return m.parentId == parentMessage.cloneId
// }
}))
if(replies.length){
return (
<>
{console.log('rendering Replies...')}
{replies.map((reply) => {
return (
<Message
origMessage={reply}
key={reply.id}
isSubClone={parentMessage.cloneId ? true : isSubClone}
firstRender={true}
/>
)
})}
{parentMessage.text && !parentMessage.cloneId && !isSubClone && (
<NewReply
parentMessage={parentMessage}
/>
)}
</>
)
}
else {
return null
}
}
// const Replies = React.memo(RepliesFunc)
const Replies = RepliesFunc
export default Message
const styles: StyleSheet = {
input: {
width: 0,
padding: 0,
paddingLeft: 10,
lineHeight: 25,
height: 25,
fontSize: 11,
boxShadow: 'none',
minWidth: 22
},
clone: {
borderBottomWidth: 2,
borderBottomColor: colors.purple
},
invisibleSpan: { // This is used for getting text width of input (for dynamic resizing of input fields)
opacity: 0,
position: 'absolute',
left: -9999,
top: -9999,
fontSize: 11
}
}
const autoSuggestTheme: StyleSheet = {
container: {
position: 'relative'
},
inputOpen: {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0
},
suggestionsContainer: {
display: 'none'
},
suggestionsContainerOpen: {
display: 'block',
position: 'absolute',
top: 25,
width: '100%',
minWidth: 400,
border: '1px solid #aaa',
backgroundColor: '#fff',
fontWeight: 300,
fontSize: 11,
borderBottomLeftRadius: 4,
borderBottomRightRadius: 4,
zIndex: 2
},
suggestionsList: {
margin: 0,
padding: 0,
listStyleType: 'none'
},
suggestion: {
cursor: 'pointer',
padding: '5px 10px'
},
suggestionHighlighted: {
backgroundColor: '#ddd'
}
}
reducers/ConversationsData.ts
import { createSlice, PayloadAction } from '#reduxjs/toolkit'
import MessageInterface from '#interfaces/message'
import axios, { AxiosRequestConfig } from 'axios'
import conversationsDataJSON from '#data/conversationsData.json'
import { AppThunk } from '#reducers/store'
import _ from 'lodash'
interface IInitialState {
loaded: boolean,
messages: MessageInterface[]
}
export const initialState: IInitialState = {
loaded: false,
messages: []
}
export const charactersDataSlice = createSlice({
name: 'conversationsData',
initialState,
reducers: {
loadData: (state, action: PayloadAction<MessageInterface[]>) => {
return state = {
loaded: true,
messages:action.payload
}
},
add: (state, { payload }: PayloadAction<{message: MessageInterface}>) => {
state.messages.push(payload.message)
},
edit: (state, { payload }: PayloadAction<{message: MessageInterface}>) => {
const updatedConversations = state.messages.map(message => {
if(message.id == payload.message.id && message.characterId == payload.message.characterId){
return message = {
...payload.message,
text: payload.message.cloneId ? '' : payload.message.text // If there's a cloneId, don't save the text since the text comes from the clone parent
}
}
else {
return message
}
})
state.messages = updatedConversations
},
remove: (state, { payload }: PayloadAction<{message: MessageInterface}>) => {
_.remove(state.messages, (message) => {
return message.id == payload.message.id && message.characterId == payload.message.characterId
})
}
}
})
const { actions, reducer } = charactersDataSlice
const { loadData, edit, add, remove } = actions
// Thunk actions
// ---------
const loadConversationsData = (): AppThunk => {
return dispatch => {
const conversationsData: MessageInterface[] = conversationsDataJSON
dispatch(loadData(conversationsData))
}
}
const updateMessage = (message: MessageInterface): AppThunk => {
return (dispatch, getState) => {
const existingMessage: MessageInterface | undefined = getState().conversationsData.messages.find((m: MessageInterface) => {
return m.id == message.id && m.characterId == message.characterId
})
// If message exists, update it
if(existingMessage){
dispatch(edit({
message: message
}))
}
// else create a new message
else {
dispatch(add({
message: message
}))
}
setTimeout(() => {
dispatch(saveConversationsData())
}, 10)
}
}
const removeMessage = (message: MessageInterface): AppThunk => {
return (dispatch, getState) => {
const children: MessageInterface[] | [] = getState().conversationsData.messages.filter((m: MessageInterface) => {
return m.parentId == message.id && m.characterId == message.characterId
})
const hasChildren = children.length > 0
// If message has children, stop
if(hasChildren){
alert('This message has children. Will not kill this message. Remove the children first.')
}
// Otherwise, go ahead and kill message
else {
dispatch(remove({
message: message
}))
setTimeout(() => {
dispatch(saveConversationsData())
}, 10)
}
}
}
export const saveConversationsData = (): AppThunk => {
return (dispatch, getState) => {
const conversationsMessages = getState().conversationsData.messages
const conversationsMessagesJSON = JSON.stringify(conversationsMessages, null, '\t')
const options: AxiosRequestConfig = {
method: 'POST',
url: 'http://localhost:8888/api/update-conversations.php',
headers: { 'content-type': 'application/json; charset=UTF-8' },
data: conversationsMessagesJSON
}
axios(options)
.catch(error => console.error('Saving conversationsData error:', error))
}
}
// Exporting it all
// ---------
export { loadConversationsData, updateMessage, removeMessage }
export default reducer
interfaces/message.ts
export default interface MessageInterface {
id: string,
characterId: string,
text: string,
cloneId: string,
parentId: string
}
export const MessageInitState: MessageInterface = {
id: '',
characterId: '',
text: '',
cloneId: '',
parentId: ''
}
Because your selector uses Array.prototype.filter you create a new array every time the messages array changes for each component.
If you would store the data in the state as nested data you can prevent this from happening. For example: {id:1, message:'hello', replies:[{id:2, message:'world', replies:[]}]}.
A simpler way is to use the memoization of reselect to see if each element in the filtered array is the same as it was last time. This will require more resources than the nested solution as it will perform the filter on every change for every branch but won't re render needlessly.
Here is the simple example:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector, defaultMemoize } = Reselect;
const initialState = { messages: [] };
//action types
const ADD = 'ADD';
//helper crating id for messages
const id = ((id) => () => ++id)(0);
//action creators
const add = (parentId, message) => ({
type: ADD,
payload: { parentId, message, id: id() },
});
const reducer = (state, { type, payload }) => {
if (type === ADD) {
const { parentId, message, id } = payload;
return {
...state,
messages: state.messages.concat({
id,
parentId,
message,
}),
};
}
return state;
};
//selectors
const selectMessages = (state) => state.messages;
//curry creating selector function that closes over message id
// https://github.com/amsterdamharu/selectors
const createSelectMessageById = (messageId) =>
createSelector([selectMessages], (messages) =>
messages.find(({ id }) => id === messageId)
);
//used to check each item in the array is same as last
// time the function was called
const createMemoizeArray = (array) => {
const memArray = defaultMemoize((...array) => array);
return (array) => memArray.apply(null, array);
};
//curry creating selector function that closes over parentId
// https://github.com/amsterdamharu/selectors
const createSelectMessagesByParentId = (parentId) => {
//memoizedArray([1,2,3]) === memoizedArray([1,2,3]) is true
//https://github.com/reduxjs/reselect/issues/451#issuecomment-637521511
const memoizedArray = createMemoizeArray();
return createSelector([selectMessages], (messages) =>
memoizedArray(
messages.filter((m) => m.parentId === parentId)
)
);
};
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const AddMessage = ({ addMessage }) => {
const [reply, setReply] = React.useState('');
return (
<div>
<label>
message:
<input
type="text"
onChange={(e) => setReply(e.target.value)}
value={reply}
/>
</label>
<button onClick={() => addMessage(reply)}>Add</button>
</div>
);
};
const AddMessageContainer = React.memo(
function AddMessageContainer({ messageId }) {
const dispatch = useDispatch();
const addMessage = React.useCallback(
(message) => dispatch(add(messageId, message)),
//dispatch in deps should not be needed but
// my linter still complains about it
[dispatch, messageId]
);
return <AddMessage addMessage={addMessage} />;
}
);
const Message = ({ message, replies }) => {
console.log('in message render', message && message.message);
return (
<div>
{message ? <h1>{message.message}</h1> : ''}
{Boolean(replies.length) && (
<ul>
{replies.map(({ id }) => (
<MessageContainer key={id} messageId={id} />
))}
</ul>
)}
{/* too bad optional chaining (message?.id) does not work on SO */}
<AddMessageContainer
messageId={message && message.id}
/>
</div>
);
};
const MessageContainer = React.memo(
function MessageContainer({ messageId }) {
const selectMessage = React.useMemo(
() => createSelectMessageById(messageId),
[messageId]
);
const selectReplies = React.useMemo(
() => createSelectMessagesByParentId(messageId),
[messageId]
);
const message = useSelector(selectMessage);
const replies = useSelector(selectReplies);
return <Message message={message} replies={replies} />;
}
);
const App = () => {
return <MessageContainer />;
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
I have made an app like Amazon and Flipkart, which filters products based on categories.
import React, { useState,useEffect} from 'react'
import { FlatList,View,Image,Text,StyleSheet,TouchableOpacity,ActivityIndicator} from 'react-native';
import {useSelector , useDispatch} from "react-redux";
import ShopComponent from "../components/ShopComponent";
import * as cartActions from "../store/actions/Cart"
import * as ProductActions from "../store/actions/Product"
import {HeaderButtons,Item} from "react-navigation-header-buttons";
import HeaderButton from "../components/UI/HeaderComponent";
import Product from '../models/Product';
import ProductsList from "../data/DataTemp";
import SearchScreen from "./SearchScreen";
const SecondScreen =(props)=>{
const [CurrentCategory, setCurrentCategory] = useState([]);
const category = props.navigation.getParam("title");
const UpdatedCategory=useSelector(state =>
state.Products.availableProduct.filter((product)=>{
return product.category == category
} )
);
const dispatch=useDispatch();
useEffect(()=>{
setCurrentCategory(UpdatedCategory)
},[])
useEffect(()=>{
dispatch(ProductActions.fetchProduct());
},[dispatch])
const loadBooks = (itemData)=>{
let newBook = {
id : itemData.item.id,
title : itemData.item.title,
Description : itemData.item.Description,
image : itemData.item.image,
Price : itemData.item.Price,
category : itemData.item.category,
}
return (
<TouchableOpacity onPress={()=>{props.navigation.navigate("Detail",
{ productId: itemData.item.id,
productTitle:itemData.item.title } );
}}>
<View style={styles.productMain}>
<View style={{width:"35%", height:200, }}>
<Image style={{width : "100%" , height:"95%" , resizeMode:"contain", borderRadius:5}}
source={{uri : itemData.item.imageUrl}} />
</View>
<View style={{ justifyContent: "space-around", alignContent:"center", marginLeft:40,}}>
<View style={{overFlow:"hidden"}}>
<Text numberOfLines={1} style={styles.text}>{itemData.item.title}</Text>
</View>
<View>
<Text numberOfLines={1} style={styles.text}>Price : Rs {itemData.item.price}</Text>
</View>
</View>
</View>
</TouchableOpacity>
)
}
return (
<View style={styles.main}>
<FlatList
data={CurrentCategory}
keyExtractor={item => item.id}
renderItem={loadBooks}
/>
</View>
)
}
SecondScreen.navigationOptions = navData=>{
return{
headerTitle:"Deep Mart",
headerLeft:(()=><HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item title="ToggleDrawer" iconName={"md-menu" } onPress={()=>{navData.navigation.toggleDrawer();}}/>
</HeaderButtons>
),
headerRight:(()=><HeaderButtons HeaderButtonComponent={HeaderButton}>
<Item title="cart" iconName={"md-cart" } onPress={()=>{navData.navigation.navigate("Cart")}}/>
</HeaderButtons>
), }
};
const styles = StyleSheet.create({
main: {
flex : 1,
padding : 10,
},
bookMain :{
marginTop:10,
width : "100%",
height:500,
borderColor:"black" ,
borderWidth:1,
borderRadius : 5
},
productMain: {
flexDirection:"row",
justifyContent:"flex-start",
borderBottomWidth:1,
marginBottom:10,
backgroundColor:"white",
borderRadius:10,
},
text : {
color:"black",
fontSize: 15,
fontWeight:"bold",
overflow:"hidden",
}
});
export default SecondScreen;
Here I have used two useEffect one for filtering title Based on categories and another for fetching Product from my ProductAction in my store.
import Product from "../../models/Product";
export const DELETE_PRODUCT = "DELETE_PRODUCTS";
export const CREATE_PRODUCT="CREATE_PRODUCT";
export const UPDATE_PRODUCT="UPDATE_PRODUCT";
export const SET_PRODUCTS = "SET_PRODUCTS"
export const SOLD_PRODUCTS = "SOLD_PRODUCTS"
export const fetchProduct=()=>{
return async dispatch => {
// any async code you want!
try {
const response= await fetch("https://direp-marct.firebaseio.com/products.json");
if (!response.ok) {
throw new Error('Something went wrong!');
}
const resData = await response.json();
const loadedproducts = [];
for(const key in resData) {
loadedproducts.push(
new Product(
key ,
"u1",
resData[key].title ,
resData[key].imageUrl ,
resData[key].description,
resData[key].price,
resData[key].mrp,
resData[key].category
)
);
}
dispatch({ type: SET_PRODUCTS, products: loadedproducts });
} catch (err) {
// send to custom analytics server
throw err;
}
};
};
export const deleteProduct=(ProductId)=>{
return async (dispatch) =>{
fetch(`https://direp-marct.firebaseio.com/products/${ProductId}.json`,{
method:"DELETE"
});
dispatch({type:DELETE_PRODUCT , pid:ProductId})
}
};
export const soldProduct=(ProductId)=>{
return async (dispatch) =>{
fetch(`https://direp-marct.firebaseio.com/products/${ProductId}.json`,{
method:"POST"
});
dispatch({type:DELETE_PRODUCT , pid:ProductId})
}
};
export const createProduct=(title , description ,imageUrl, price , mrp ,category)=>{
return async (dispatch)=>{
, {
const response= await fetch("https://direp-marct.firebaseio.com/products.json" ,
{
method:"POST",
header:{
"Content-Type":"Application/json"
},
body:JSON.stringify({
title,
description,
imageUrl,
price,
mrp,
category
})
});
const resData = await response.json();
dispatch({
type:CREATE_PRODUCT,
productData: {
id:resData.name,
title ,
imageUrl ,
description ,
price,
mrp,
category
}});
}
};
export const updateProduct=(id, title , description,imageUrl , price ,mrp, category)=>{
return async (dispatch)=>{
const response=await fetch(`https://direp-marct.firebaseio.com/products/${id}.json`,{
method:"PATCH",
header:{
"Content-type":"application.json"
},
body:JSON.stringify({
title,
description,
imageUrl,
price,
mrp,
category
})
})
dispatch( {
type:UPDATE_PRODUCT,
pid:id ,
productData:{ title,description, imageUrl , price,mrp ,category}}
)
}
};
Here is My Reducer
import PRODUCTS from "../../data/dummy-data";
import {DELETE_PRODUCT , CREATE_PRODUCT , UPDATE_PRODUCT, SET_PRODUCTS , SOLD_PRODUCTS} from "../../store/actions/Product";
import Product from "../../models/Product";
const initialState={
availableProduct : [],
userProduct : []
};
export default (state=initialState , action) =>{
switch (action.type){
case SET_PRODUCTS:
return{
availableProduct:action.products,
userProduct:action.products
}
case CREATE_PRODUCT:
const newProduct = new Product(
action.productData.id,
'u1',
action.productData.title,
action.productData.imageUrl,
action.productData.description,
action.productData.price,
action.productData.mrp,
action.productData.category
);
return {
...state,
availableProduct: state.availableProduct.concat(newProduct),
userProduct: state.userProduct.concat(newProduct)
};
case UPDATE_PRODUCT:
const productIndex = state.userProduct.findIndex(
prod => prod.id === action.pid
);
const updatedProduct = new Product(
action.pid,
state.userProduct[productIndex].ownerId,
action.productData.title,
action.productData.imageUrl,
action.productData.description,
action.productData.price,
action.productData.mrp,
action.productData.category
);
const updatedUserProducts = [...state.userProduct];
updatedUserProducts[productIndex] = updatedProduct;
const availableProductIndex = state.availableProduct.findIndex(
prod => prod.id === action.pid
);
const updatedAvailableProducts = [...state.availableProduct];
updatedAvailableProducts[availableProductIndex] = updatedProduct;
return {
...state,
availableProduct: updatedAvailableProducts,
userProduct: updatedUserProducts
};
case DELETE_PRODUCT:
return {
...state,
userProduct: state.userProduct.filter(
product => product.id !== action.pid
),
availableProduct: state.availableProduct.filter(
product => product.id !== action.pid
)
};
}
return state;
};
The problem is that when I run my app and click on a particular category (for example- Baby Care ) then at first no product is fetched and I see a blank screen. but when I click the second time my product gets fetched and I see the result.
And I want that as I click on the category instead of the blank page I want to see fetched products initially.
I think the problem is with UseEffect and I don't know what to do as I am new to react-native.
Any Help will be appreciated thanks in advance.
The problem is with these two pieces and how they work together:
const UpdatedCategory=useSelector(state =>
state.Products.availableProduct.filter((product)=>{
return product.category == category
} )
);
useEffect(()=>{
setCurrentCategory(UpdatedCategory)
},[])
UpdatedCategory comes from a useSelector hook, which means that it's value gets updated when the state changes.
But this useEffect has an empty dependency array [] which means that the setCurrentCategory gets called only once when the component is mounted.
You want the CurrentCategory to stay updated based on changes in the state, so you need to have UpdatedCategory in your dependencies.
useEffect(()=>{
setCurrentCategory(UpdatedCategory)
},[UpdatedCategory])
I need to access my current cart state which is just a list of products that have
been added to the cart, so that I check ids in order to calculate quantity for duplicate products. I realize that one of my issues here is that i've initialized itemsInCart with an empty array but i'm not sure what else to do here since, state can't be destructured without it.
cartReducer.js
const itemsInCart = []
export const cartReducer = (state = itemsInCart, action) => {
const { type, payload } = action;
switch (type) {
case "ADD_TO_CART":
return [
...state,
{
imgUrl: payload.imgUrl,
name: payload.name,
price: payload.price,
quantity: payload.quantity
},
];
default:
}
return state;
};
Product.js
Clicking the button dispatches the 'ADD_TO_CART' action, adds new products to our cart in state.
import React from 'react';
import {useDispatch} from 'react-redux';
const Product = ({imgUrl, name, price, id }) => {
const dispatch = useDispatch()
const addToCart = () => {
dispatch({
type: "ADD_TO_CART",
payload: {
imgUrl: imgUrl,
name: name,
price: price,
id: id
}
})
}
return (
<div
key={id}
style={{
textAlign: "center",
display: "flex",
border: "1px solid",
marginBottom: "2rem",
flexDirection: 'column'
}}
>
<img
src={imgUrl}
style={{ height: "5rem", width: "5rem" }}
alt="The product"
/>
<h3>{name}</h3>
<p>${price}.00</p>
{id}
<button onClick={()=>addToCart()}>Add To Cart</button>
</div>
);
}
export default Product;
InCartList.js
Renders list of items in my cart inside CartContainer
import React from "react";
import { useSelector } from "react-redux";
import CartItem from "./CartItem";
const ProductList = () => {
const allState = useSelector((state) => state.cart);
const renderProducts = () => {
return allState.map((product) => {
return (
<CartItem id={product.id} quantity={product.quantity}key={product.id} name={product.name} price={product.price}/>
);
});
};
return <>{renderProducts()}</>;
};
export default ProductList;
You shouldn't place any logic inside reducer (reducer should only pure function)
You can try to get state you want before dispatch action ADD_TO_CART
use getStateToProps function
use store which should be exported when initialized inside App component (I guess)
export const store = configureAppStore(history);