Translating context to redux with setTimeout - javascript

I have this context:
interface AlertContextProps {
show: (message: string, duration: number) => void;
}
export const AlertContext = createContext<AlertContextProps>({
show: (message: string, duration: number) => {
return;
},
});
export const AlertProvider: FC<IProps> = ({ children }: IProps) => {
const [alerts, setAlerts] = useState<JSX.Element[]>([]);
const show = (message: string, duration = 6000) => {
let alertKey = Math.random() * 100000;
setAlerts([...alerts, <Alert message={message} duration={duration} color={''} key={alertKey} />]);
setTimeout(() => {
setAlerts(alerts.filter((i) => i.key !== alertKey));
}, duration + 2000);
};
return (
<>
{alerts}
<AlertContext.Provider value={{ show }}>{children}</AlertContext.Provider>
</>
);
};
which I need to "translate" into a redux slice. I got a hang of everything, apart from the show method. What would be the correct way to treat it? I was thinking about a thunk, but it's not really a thunk. Making it a reducer with setTimeout also seems like an ugly thing to do. So how would you guys do it?
My code so far:
type Alert = [];
const initialState: Alert = [];
export const alertSlice = createSlice({
name: 'alert',
initialState,
reducers: {
setAlertState(state, { payload }: PayloadAction<Alert>) {
return payload;
},
},
});
export const { setAlertState } = alertSlice.actions;
export const alertReducer = alertSlice.reducer;

The timeout is a side effect so you could implement that in a thunk.
You have an action that shows an alert message that has a payload of message, id and time to display, when that time runs out then the alert message needs to be removed so you need a remove alert message action as well that is dispatched from the thunk with a payload of the id of the alert message.
I am not sure why you add 2 seconds to the time to hide the message duration + 2000 since the caller can decide how long the message should show I don't think it should half ignore that value and randomly add 2 seconds.
Here is a redux example of the alert message:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = {
messages: [],
};
//action types
const ADD_MESSAGE = 'ADD_MESSAGE';
const REMOVE_MESSAGE = 'REMOVE_MESSAGE';
//action creators
const addMessage = (id, text, time = 2000) => ({
type: ADD_MESSAGE,
payload: { id, text, time },
});
const removeMessage = (id) => ({
type: REMOVE_MESSAGE,
payload: id,
});
//id generating function
const getId = (
(id) => () =>
id++
)(1);
const addMessageThunk = (message, time) => (dispatch) => {
const id = getId();
dispatch(addMessage(id, message, time));
setTimeout(() => dispatch(removeMessage(id)), time);
};
const reducer = (state, { type, payload }) => {
if (type === ADD_MESSAGE) {
return {
...state,
messages: state.messages.concat(payload),
};
}
if (type === REMOVE_MESSAGE) {
return {
...state,
messages: state.messages.filter(
({ id }) => id !== payload
),
};
}
return state;
};
//selectors
const selectMessages = (state) => state.messages;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
//simple implementation of thunk (not official redux-thunk)
({ dispatch }) =>
(next) =>
(action) =>
typeof action === 'function'
? action(dispatch)
: next(action)
)
)
);
const App = () => {
const messages = useSelector(selectMessages);
const dispatch = useDispatch();
return (
<div>
<button
onClick={() =>
dispatch(addMessageThunk('hello world', 1000))
}
>
Add message
</button>
<ul>
{messages.map((message) => (
<li key={message.id}>{message.text}</li>
))}
</ul>
</div>
);
};
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>
<div id="root"></div>

#HMR's use of a thunk is fine, but I don't like what they've done to your reducer. You're already using redux-toolkit which is great! redux-toolkit actually includes and exports a nanoid function which they use behind the scenes to create unique ids for thunks. You can use that instead of Math.random() * 100000.
I always start by thinking about types. What is an Alert? You don't want to store the <Alert/> because a JSX.Element is not serializable. Instead you should just store the props. You'll definitely store the message and key/id. If you handle expiration on the front-end then you would also store the duration, but if the expiration is handled by a thunk then I don't think you need it in the redux state or component props.
It seems like you want to allow multiple alerts at one time, so return payload is not going to cut it for your reducer. You'll need to store an array or a keyed object will all of your active alerts.
You absolute should not use setTimeout in a reducer because that is a side effect. You can use it either in a thunk or in a useEffect in the Alert component. My inclination is towards the component because it seems like the alert should probably be dismissible as well? So you can use the same function for handling dismiss clicks and automated timeouts.
We can define the info that we want to store for each alert.
type AlertData = {
message: string;
id: string;
duration: number;
}
And the info that we need to create that alert, which is the same but without the id because we will generate the id in the reducer.
type AlertPayload = Omit<AlertData, 'id'>
Our state can be an array of alerts:
const initialState: AlertData[] = [];
We need actions to add a new alert and to remove an alert once it has expired.
import { createSlice, PayloadAction, nanoid } from "#reduxjs/toolkit";
...
export const alertSlice = createSlice({
name: "alert",
initialState,
reducers: {
addAlert: (state, { payload }: PayloadAction<AlertPayload>) => {
const id = nanoid(); // create unique id
state.push({ ...payload, id }); // add to the state
},
removeAlert: (state, { payload }: PayloadAction<string>) => {
// filter the array -- payload is the id
return state.filter((alert) => alert.id !== payload);
}
}
});
export const { addAlert, removeAlert } = alertSlice.actions;
export const alertReducer = alertSlice.reducer;
So now to the components. What I have in mind is that you would use a selector to select all of the alerts and then each alert will be responsible for its own expiration.
export const AlertComponent = ({ message, duration, id }: AlertData) => {
const dispatch = useDispatch();
// function called when dismissed, either by click or by timeout
// useCallback is just so this can be a useEffect dependency and won't get recreated
const remove = useCallback(() => {
dispatch(removeAlert(id));
}, [dispatch, id]);
// automatically expire after the duration, or if this component unmounts
useEffect(() => {
setTimeout(remove, duration);
return remove;
}, [remove, duration]);
return (
<Alert
onClose={remove} // can call remove directly by clicking the X
dismissible
>
<Alert.Heading>Alert!</Alert.Heading>
<p>{message}</p>
</Alert>
);
};
export const ActiveAlerts = () => {
const alerts = useSelector((state) => state.alerts);
return (
<>
{alerts.map((props) => (
<AlertComponent {...props} key={props.id} />
))}
</>
);
};
I also made a component to create alerts to test this out and make sure that it works!
export const AlertCreator = () => {
const dispatch = useDispatch();
const [message, setMessage] = useState("");
const [duration, setDuration] = useState(8000);
return (
<div>
<h1>Create Alert</h1>
<label>
Message
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</label>
<label>
Duration
<input
type="number"
step="1000"
value={duration}
onChange={(e) => setDuration(parseInt(e.target.value, 10))}
/>
</label>
<button
onClick={() => {
dispatch(addAlert({ message, duration }));
setMessage("");
}}
>
Create
</button>
</div>
);
};
const App = () => (
<div>
<AlertCreator />
<ActiveAlerts />
</div>
);
export default App;
Code Sandbox Link

Related

How to handle state in useEffect from a prop passed from infinite scroll component

I have a React component using an infinite scroll to fetch information from an api using a pageToken.
When the user hits the bottom of the page, it should fetch the next bit of information. I thought myself clever for passing the pageToken to a useEffect hook, then updating it in the hook, but this is causing all of the api calls to run up front, thus defeating the use of the infinite scroll.
I think this might be related to React's derived state, but I am at a loss about how to solve this.
here is my component that renders the dogs:
export const Drawer = ({
onClose,
}: DrawerProps) => {
const [currentPageToken, setCurrentPageToken] = useState<
string | undefined | null
>(null);
const {
error,
isLoading,
data: allDogs,
nextPageToken,
} = useDogsList({
pageToken: currentPageToken,
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting) {
setCurrentPageToken(nextPageToken);
}
},
[nextPageToken],
);
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
}, [handleObserver]);
return (
<Drawer
onClose={onClose}
>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
useDogsList essentially looks like this with all the cruft taken out:
import { useEffect, useRef, useState } from 'react';
export const useDogsList = ({
pageToken
}: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
null,
);
const [allDogs, setAllDogs] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const result =
await myClient.listDogs(
getDogsRequest,
{
token,
},
);
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken);
// if API returns a pageToken, that means there are more dogs to add to the list
if (nextPageToken) {
setAllDogs((previousDogList) => [
...(previousDogList ?? []),
...newDogs,
]);
}
}
} catch (responseError: unknown) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
};
fetchData();
}, [ pageToken, nextPageToken]);
return {
data: allDogs,
nextPageToken,
error,
isLoading,
};
};
Basically, the api call returns the nextPageToken, which I want to use for the next call when the user hits the intersecting point, but because nextPageToken is in the dependency array for the hook, the hook just keeps running. It retrieves all of the data until it compiles the whole list, without the user scrolling.
I'm wondering if I should be using useCallback or look more into derivedStateFromProps but I can't figure out how to make this a "controlled" component. Does anyone have any guidance here?
I suggest a small refactor of the useDogsList hook to instead return a hasNext flag and fetchNext callback.
export const useDogsList = ({ pageToken }: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
pageToken // <-- initial token value for request
);
const [allDogs, setAllDogs] = useState([]);
// memoize fetchData callback for stable reference
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await myClient.listDogs(getDogsRequest, { token: nextPageToken });
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken ?? null);
setAllDogs((previousDogList) => [...previousDogList, ...newDogs]);
} catch (responseError) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
}, [nextPageToken]);
useEffect(() => {
fetchData();
}, []); // call once on component mount
return {
data: allDogs,
hasNext: !!nextPageToken, // true if there is a next token
error,
isLoading,
fetchNext: fetchData, // callback to fetch next "page" of data
};
};
Usage:
export const Drawer = ({ onClose }: DrawerProps) => {
const { error, isLoading, data: allDogs, hasNext, fetchNext } = useDogsList({
pageToken // <-- pass initial page token
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting && hasNext) {
fetchNext(); // <-- Only fetch next if there is more to fetch
}
},
[hasNext, fetchNext]
);
useEffect(() => {
const option = {
root: null,
rootMargin: "20px",
threshold: 0
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
// From #stonerose036
// clear previous observer in returned useEffect cleanup function
return observer.disconnect;
}, [handleObserver]);
return (
<Drawer onClose={onClose}>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
Disclaimer
Code hasn't been tested, but IMHO it should be pretty close to what you are after. There may be some minor tweaks necessary to satisfy any TSLinting issues, and getting the correct initial page token to the hook.
While Drew and #debuchet's answers helped me improve the code, the problem around multiple renders ended up being solved by tackling the observer itself. I had to disconnect it afterwards
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
return () => {
observer.disconnect();
};
}, [handleObserver]);

How to disable an active item after dispatch?

I would like to create notifications that expire after a set amount of seconds.
I have created a property which is 'active' and when toggled to false it will hide.
Ideally, it would be nice to have the expiry automatically set in the slice, i.e. run the disable reducer within the runtime of the notify reducer but i'm not sure this is good practice, and am not sure how to pull it off.
What is the best way to pull this off? I was thinking of adding an expiry date on each item but since the 'active' field is already there I would like to set a timeout and toggle it to false after 3 seconds..
Notification component:
export function Notification() {
const dispatch = useDispatch();
function disableAlert(id: number) {
dispatch(disable({'id' : id}));
}
const notification_list = useSelector(getNotification);
if (notification_list && notification_list.length > 0) {
return notification_list.map((notification: any, index: number) =>
notification.active ?
<Alert onClose={() => disableAlert(index)} style={{bottom: 50 * index}} severity={notification.mode}>{notification.message}</Alert> :
console.log(notification)
)
}
return <></>
}
Currently I have these slices:
const disableMessage = (state: any, message_id: number) => {
return state.messages.map((message:any) => message.id === message_id ?
{...message, active: !message.active} :
message
);
}
export const notificationSlice = createSlice({
name: 'notification',
initialState: initialState,
reducers: {
notify: (state, action) => {
const { message, mode, active } = action.payload;
state.messages.push({id: state.messages.length , message : message, mode: mode, active: active});
},
disable: (state, action) => {
const { id } = action.payload;
state.messages = disableMessage(state, id);
}
}
})
It is convention that reducers never contain any type of logic. I recommend to stick with this.
This leaves either the action or the Notification component. For me it makes more sense to tie the disable to the rendering of the individual notification so I would start the timeout there.
Ideally, you can split your <Alert/> component into the presentation and logic. Something similar to:
const NotificationAlert = ({ disableAlert, id }) => {
const notification = useSelector((state) => selectNotificationById(state, id));
const handleClick = useCallback(() => {
disableAlert(id);
}, [disableAlert, id]);
useEffect(() => {
setTimeout(() => disableAlert(id), 3000);
}, [disableAlert]);
return (
<Alert
onClose={handleClick}
style={{bottom: 50 * id}}
severity={notification.mode}>{notification.message}</Alert>
};
And
export function Notification() {
const dispatch = useDispatch();
// memoize handler with useCallback
const disableAlert = useCallback((id: number) => {
dispatch(disable({'id' : id}));
}, [dispatch]);
// Filter for active notifications already in your selector
const notificationIds = useSelector(getActiveNotificationIds);
return notificationIds.map((id) =>
<NotificationAlert disableAlert={disableAlert} id={id} />
);
}
Also, make sure your disableAlert action is setting active to false rather than toggling it!

Redux Toolkit State issue when sending to child component

I am creating react redux application using redux toolkit and I'm passing some props to child component, it supposed to be one post because I'm using a map in parent component and passing one data to each component.
I'm trying to do Edit button and when clicking the "Edit button" trying to send ID to redux store but there is an error. If anyone know the answer please let me know.
Below is my redux slice:
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import axios from "axios";
const initialState = {
allPosts: [],
loading: "idle",
error: "",
currentId: "",
};
export const fetchAlltAsync = createAsyncThunk(
"allposts",
async (_, thunkAPI) => {
try {
const response = await axios.get("http://localhost:5000/posts/");
// The value we return becomes the `fulfilled` action payload
return response.data;
} catch (error) {
throw thunkAPI.rejectWithValue({ error: error.message });
}
}
);
export const postsingleAsync = createAsyncThunk(
"postsingleAsync",
async (post, { dispatch }) => {
const response = await axios.post("http://localhost:5000/posts/", post);
return response.data;
}
);
export const idsingleAsync = createAsyncThunk(
"idsingleAsync",
async (id, updatedpost) => {
const response = await axios.patch(
`http://localhost:5000/posts/${id}`,
updatedpost
);
return response.data;
}
);
export const postSlice = createSlice({
name: "posts",
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
// Use the PayloadAction type to declare the contents of `action.payload`
newsetcurrentId: (state, action) => {
state.currentId = action.payload;
},
},
// The `extraReducers` field lets the slice handle actions defined elsewhere,
// including actions generated by createAsyncThunk or in other slices.
extraReducers: (builder) => {
builder.addCase(fetchAlltAsync.pending, (state) => {
state.allPosts = [];
state.loading = "Loading";
});
builder.addCase(fetchAlltAsync.fulfilled, (state, action) => {
state.allPosts = action.payload;
state.error += "Loaded";
});
builder.addCase(fetchAlltAsync.rejected, (state, action) => {
state.allposts = "data not loaded";
state.loading = "error";
state.error = action.error.message;
});
builder.addCase(idsingleAsync.fulfilled, (state, action) => {
state.currentId = action.payload;
});
},
});
export const { setcurrentId, newsetcurrentId } = postSlice.actions;
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
export const selectCount = (state) => state.counter.value;
// We can also write thunks by hand, which may contain both sync and async logic.
// Here's an example of conditionally dispatching actions based on current state.
export const incrementIfOdd = (amount) => (dispatch, getState) => {};
export default postSlice.reducer;
Below is my parent component:
import React, { useEffect, useState } from "react";
import Post from "./Post";
import { useSelector, useDispatch } from "react-redux";
const Posts = ({ SETCURRENTID, CURENTID }) => {
// const dispatch = useDispatch();
const posts = useSelector((state) => state.posts.allPosts);
return (
<div>
{posts &&
posts.map(({ _id, ...rest }) => (
<Post key={_id} rest={rest} id={_id} />
))}
</div>
);
};
export default Posts;
This is my child component:
import React from "react";
import moment from "moment";
import { idsingleAsync, newsetcurrentId } from "../../features/postSlice";
import { useSelector, useDispatch } from "react-redux";
const Post = ({ rest, _id }) => {
const dispatch = useDispatch();
console.log(rest, "gff");
//const { id } = this.rest._id;
const handleClick = () => dispatch(newsetcurrentId());
return (
<div>
<h1>{rest.title}</h1>
<img
style={{ maxWidth: "250px", border: "12px solid purple" }}
alt="d"
src={rest.selectedFile}
/>
<h2>{moment(rest.createdAt).fromNow()}</h2>
<button onClick={() => dispatch(newsetcurrentId(rest._id))}> edit</button>
<h5>{rest.tags.map((tag) => `#${tag} `)}</h5>
<h5 onClick={() => {}}>{rest.likeCount}</h5>
<button onClick={() => {}}>Delete</button>
</div>
);
};
export default Post;
This is the redux error:
requestId(pin):undefined
TL;DR
Instead of rest._id , try passing the id prop to your newsetcurrentId dispatch:
const Post = ({ rest, id }) => { //Change _id to id
const dispatch = useDispatch();
const handleClick = () => dispatch(newsetcurrentId());
return (
<div>
<h1>{rest.title}</h1>
<img
style={{ maxWidth: "250px", border: "12px solid purple" }}
alt="d"
src={rest.selectedFile}
/>
<h2>{moment(rest.createdAt).fromNow()}</h2>
{/* pass id here */}
<button onClick={() => dispatch(newsetcurrentId(id))}> edit</button>
<h5>{rest.tags.map((tag) => `#${tag} `)}</h5>
<h5 onClick={() => {}}>{rest.likeCount}</h5>
<button onClick={() => {}}>Delete</button>
</div>
);
};
Explanation
When you are doing this destructuring:
posts.map(({ _id, ...rest }) => ( your rest object will actually contain all the post properties apart from _id so you don't actually have rest._id which you are trying to access on your Post child.
Additionally, you are passing id={_id} as a prop from the parent to the child, so you don't actually have an _id prop on your Post component (change it to id).

Redux state not updating after action dispatched

I have a form for users to enter their details and press submit. This is supposed to dispatch an action and update the state by .concat() a class to it. Unfortunately the state isn't updating and I don't know why. If I take out useCallBack() or useEffect() from the code , the emulator freezes and I suspect infinite loops.
Redux Reducer
// Initialised class
import newAccount from '../../models/newAccount'
import { CREATE_ACCOUNT } from '../actions/meals'
const initialState = {
account: [],
}
const addPerson = (state=initialState, action) =>{
switch(action.type){
case CREATE_ACCOUNT:
const newAccount = new newAccount(
Date.now().toString(),
action.accountData.name,
action.accountData.image,
action.accountData.email,
action.accountData.password
)
return { ...state, account: state.account.concat(newAccount) }
default:
return state
}
}
export default addPerson
Redux action
export const CREATE_ACCOUNT = 'CREATE_ACCOUNT'
export const newAccount = (Id,name,image, email, password) => {
return {type: CREATE_ACCOUNT, accountData:{
Id: Date.now().toString(),
name: name,
image: image,
email: email,
password: password
}
}
}
The class
class newAccount {
constructor(
id,
name,
image,
email,
password
){
this.id = id;
this.name = name;
this.image = image;
this.email = email;
this.password = password;
}
}
export default newAccount
The Component
import React, { useState, useCallback, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {newAccount} from '../Store/actions/accounts'
import ImagePicker from '../Components/ImagePicker'
const AddScreen = (props) => {
const dispatch = useDispatch()
const [name, setName] = useState('')
const [selectedImage, setSelectedImage] = useState('')
const email = useSelector(state => state.account.email)
const password = useSelector(state => state.account.password)
const handleSubmit = useCallback(() => {
dispatch(newAccount(Date.now(),name,selectedImage,email,password))
},[dispatch, name, selectedImage, email, password])
useEffect(() => { handleSubmit
props.navigation.setParams({handleSubmit: handleSubmit})
},[handleSubmit])
return (
<View style={styles.container}>
<View style={styles.card}>
<ImagePicker onImageSelected={selectedImage} />
<AddForm email={email} password={password}/>
<TextInput
onChangeText={name => setName(name)}
value={name}
/>
</View>
</View>
)
}
export default AddScreen
AddScreen.navigationOptions = (navigationData) => {
const submit = navigationData.navigation.getParam('handleSubmit')
return {
headerTitle: 'Create Account',
headerRight: () => (
<TouchableOpacity onPress={submit}>
<Text style={styles.createOrange}>Create</Text>
</TouchableOpacity>
)
}
}
I really don't know why it's not updating .
first of all, you shouldn't store classes in the redux store, the store should only exists of plain objects. but if you really want to store the class:
The real problem seams to be return { ...state, account: state.account.concat(newAccount) }. here you concat the existing array with the new class, but that doesn't work.
your store looks like this if you do so:
{
account: [{
email: "..."
id: "..."
image: "..."
name: "..."
password: "...
}],
}
so your selector (state.account.email) will return undefined. you can use (state.account[0].email)
or you can fix it by fixing the real problem:
return { ...state, account: newAccount }
also your initialState shouldn't be a an array for account as it will never be an array, it will be an Account class (this is why you don't get an error by what you are doing). set it to null.
const initialState = {
account: null,
}
I really don't know why this doesn't work. Just want to give you an advice to make it more simple and clearer (from my point of view):
You can drop side effects like useEffect. To achieve this just move local state to redux state and then you will be able to just dispatch the action from your navigationOptions component. It could look like:
const AddScreen = () => {
const name = useSelector(...);
...
const password = useSelector(...);
// trigger action on something changes, for instance like that:
const onChange = (key, value) = dispatch(newAccountChange({[key]: value}))
// return tree of components
}
export const submitNewAccount = () => {
return (dispatch, getState) => {
const { id, name, ... } = getState().account;
dispatch(newAccount(id, name, ...));
};
}
AddScreen.navigationOptions = (navigationData) => {
const dispatch = useDispatch();
const submit = dispatch(submitNewAccount());
...
}
I used redux-thunk in this example.
I believe, this approach will give you more flexible way to debug and extend your business logic.

How to dispatch action in Custom Hooks by useReducer and useContext?

I created a sample for a button toggle.
This is done by useContext (store the data) and useReducer (process the data). and it is working fine.
Here's the CodeSandBox Link to how it works.
version 1 is just dispatch when clicking the button.
Then I created a version 2 of toggling. basically just put the dispatch inside a custom hook. but somehow, it doesn't work.
// context
export const initialState = { status: false }
export const AppContext = createContext({
state: initialState,
dispatch: React.dispatch
})
// reducer
const reducer = (state, action) => {
switch (action.type) {
case 'TOGGLE':
return {
...state,
status: action.payload
}
default:
return state
}
}
//custom hook
const useDispatch = () => {
const {state, dispatch} = useContext(AppContext)
return {
toggle: dispatch({type: 'UPDATE', payload: !state.status})
// I tried to do toggle: () => dispatch(...) as well
}
}
// component to display and interact
const Panel = () => {
const {state, dispatch} = useContext(AppContext)
// use custom hook
const { toggle } = useDispatch()
const handleChange1 = () => dispatch({type: 'TOGGLE', payload: !state.status})
const handleChange2 = toggle // ERROR!!!
// and I tried handleChange2 = () => toggle, or, handleChange2 = () => toggle(), or handleChange2 = toggle()
return (
<div>
<p>{ state.status ? 'On' : 'Off' }</p>
<button onClick={handleChange1}>change version 1</button>
<button onClick={handleChange2}>change version 2</button>
</div>
)
}
// root
export default function App() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<AppContext.Provider value={{state, dispatch}}>
<div className="App">
<Panel />
</div>
</AppContext.Provider>
);
}
Not sure what's going there. but I think there's something wrong with the dispatched state.
(I tried it works if the payload is not processing state, like some hard code stuff, so the dispatch should be fired at this moment)
Could someone give me a hand? Appreciate!!!
You are correct that toggle needs to be a function but you are dispatching action type UPDATE and the reducer doesn't do anything with that action.
Dennis is correct that there is no point in the initial value you are giving the context and may as well leave it empty as the provider will provide the value.
The useMemo suggestion from Dennis will not optimize your example since App re renders when state changes so the memoized value will never be used.
Here is a working example of your code with comments what I changed:
const { createContext, useReducer, useContext } = React;
const initialState = { status: false };
//no point in setting initial context value
const AppContext = createContext();
const reducer = (state, action) => {
switch (action.type) {
case 'TOGGLE':
return {
...state,
status: action.payload,
};
default:
return state;
}
};
const useDispatch = () => {
const { state, dispatch } = useContext(AppContext);
return {
//you were correct here, toggle
// has to be a function
toggle: () =>
dispatch({
//you dispatch UPDATE but reducer
// is not doing anything with that
type: 'TOGGLE',
payload: !state.status,
}),
};
};
const Panel = () => {
const { state, dispatch } = useContext(AppContext);
const { toggle } = useDispatch();
const handleChange1 = () =>
dispatch({ type: 'TOGGLE', payload: !state.status });
const handleChange2 = toggle; // ERROR!!!
return (
<div>
<p>{state.status ? 'On' : 'Off'}</p>
<button onClick={handleChange1}>
change version 1
</button>
<button onClick={handleChange2}>
change version 2
</button>
</div>
);
};
function App() {
const [state, dispatch] = useReducer(
reducer,
initialState
);
return (
<AppContext.Provider value={{ state, dispatch }}>
<div className="App">
<Panel />
</div>
</AppContext.Provider>
);
}
ReactDOM.render(<App />, 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>
<div id="root"></div>
Well, there no such thing React.dispatch. Its value is undefined
export const AppContext = createContext({
state: initialState,
// useless
// dispatch: undefined
dispatch: React.dispatch
});
// dispatch function won't trigger anything.
const {state, dispatch} = useContext(AppContext);
version 1 is actually how context should be used, although usually, you will want to add an extra memoization step (depending on the use case), because on every render you assign a new object {state,dispatch} which always will cause a render even though state may be the same.
See such memoization use case example.
If my point wasn't clear, see HMR comment:
Strategic useMemo should be used, if many components access the
context then memoizing is a good idea when the component with the
provider re-renders for reasons other than changing the context.

Categories

Resources