I am trying to use a reducer to update a progress bar on a React/ Typescript app I am building. I am using the useReducer hook.
I have a progress bar that sits in the header which I am trying to get to 'listen' to the reducer state and rerender with the correct progress bar values that are given to it. At the moment though, the 'subscriber' component (so to speak) is not picking up the updated value. I know the value is being updated and recieved in the reducer as I can see it in their when I log out the action in this reducer.
Here is my code:
Reducer:
import ProgressBarTitleEnum from '../lib/enums/progressBarTitleEnum';
export const UPDATE_PROGRESS_BAR = 'updateProgressBar';
export interface IProgressBarState {
name: string;
value: number;
}
interface IAction {
type: string;
payload: IProgressBarState;
}
export const initialState: IProgressBarState = {
name: ProgressBarTitleEnum.INVESTOR_PROFILE,
value: 0,
};
export const reducer = (state = initialState, action: IAction): IProgressBarState => {
switch (action.type) {
case UPDATE_PROGRESS_BAR:
return {
...initialState,
name: action.payload.name,
value: action.payload.value,
};
default:
return state;
}
};
export function updateProgressBarAction(progressBarPayload: IProgressBarState): IAction {
return {
type: UPDATE_PROGRESS_BAR,
payload: progressBarPayload,
};
}
The component which is dispatching the action
import MainLayout from '../../components/layout/MainLayout';
import {
initialState as progressBarState,
reducer as progressBarReducer,
updateProgressBarAction,
} from '../../reducers/progressBarReducer';
import ProgressBarTitleEnum from '../../lib/enums/progressBarTitleEnum';
interface IStepsCompletedInterface {
investmentStyle: boolean;
investmentPeriod: boolean;
investmentRange: boolean;
investmentAmount: boolean;
}
export default function InvestorProfile(): JSX.Element {
const [progressBarStore, progressBarDispatch] = useReducer(progressBarReducer, progressBarState);
useEffect(() => {
progressBarDispatch(
updateProgressBarAction({
name: ProgressBarTitleEnum.INVESTOR_PROFILE,
value: 1,
}),
);
}, []);
And the progressBar component that is to listen to the state changes and render accordingly:
import { initialState as progressBarState, reducer as progressBarReducer } from '../../reducers/progressBarReducer';
// Can probably move it to react context
export default function ProgressBar(): JSX.Element {
const [progressBarStore, progressBarDispatch] = useReducer(progressBarReducer, progressBarState);
const isMobile = useMediaQuery({
query: `(max-device-width: ${mediaQueriesMap.get('mobile')}px)`,
});
console.log(progressBarStore);
const markUp = (
<div style={styles.container}>
{Array(4)
.fill(0)
.map((_, i) => {
let circleMarkup;
if (progressBarStore.value === i) {
circleMarkup = (
Can anyonewhy the updated state is not being picked up in the progress bar component? Also, do I need to create a local state variable to hold changes and combine with useEffect in the progress bar component to cause renders or does useReducer cause a rerender?
Thanks
Related
React question
I have this component and I am trying to set the setIdForCSV state in redux slices as the other states in the app but I am having some issues to understand it. Below the question.
function SelectWithHook({ id }: { id: string }) {
const {
data: productSchema,
isLoading: loadingProductSchema,
isSuccess: isSuccessProductSchema
} = useGetProductSchemasQuery(id);
useEffect(() => {
}, [productSchema]);
const [idForCSV, setIdForCSV] = useState<string | null>(null);
const {
data: batchTemplate,
isLoading: loadingBatchTemplate,
isSuccess: isSuccessBatchTemplate
} = useFetchBatchTemplateQuery(idForCSV);
return (
<CreateBatchTemplateSelector
data={
productSchema && Array.isArray(productSchema.data)
? productSchema.data.map((i: { label: string }) => i.label)
: []
}
onValueChange={(e: string) => {
const schema = productSchema.data.map(
(i: { label: string }) => i.label === e
);
setIdForCSV(schema.id);
}}
/>
);
}
so far I have this slice:
import { createSlice, PayloadAction } from '#reduxjs/toolkit';
export const initialState = {
id: null
};
const slice = createSlice({
name: 'csv',
initialState,
reducers: {
setTransactionsPage: (state, { payload }: PayloadAction<string | null>) => {
state.setIdForCSV = payload;
},
});
export const {
setIdForCSV,
} = slice.actions;
I am wondering how to set it up so I can import the idForCSV in my component and in another one, I am confused about how to set up the reducers and the initial state. Basically the initial state is null by default, but when I update it in my component, it becomes a string - if it is a string I am able to run a query, and if it is null I will not. But I need to also check in another component if the state includes a string so I can press a submit button and download a CSV. I am not sure if this is the right approach. I am slightly confused about how to create this simple slice. Any clues ?
I had implemented useContext + useReducer and I found that I was getting re-renders when only dispatch changed.
I could have two separate components and if one dispatch was triggered both component got changed.
Example:
Both increment and decrement got rendered on each state update.
I found this article that I have followed but I still get the same result.
the code:
export default function App() {
return (
<MyContextProvider>
<Count />
<ButtonIncrement /> <br /> <br />
<ButtonDecrement />
</MyContextProvider>
);
}
Provider:
import * as React from 'react';
import {
InitalState,
ApplicationContextDispatch,
ApplicationContextState,
} from './Context';
import { applicationReducer } from './Reducer';
export const MyContextProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(applicationReducer, InitalState);
return (
<ApplicationContextDispatch.Provider value={{ dispatch }}>
<ApplicationContextState.Provider value={{ state }}>
{children}
</ApplicationContextState.Provider>
</ApplicationContextDispatch.Provider>
);
};
Context:
import React, { Dispatch } from 'react';
export enum ApplicationActions {
increment = 'increment',
decrement = 'decrement',
notification = 'Notification',
}
export type ActionType = ApplicationActions;
export const ActionTypes = { ...ApplicationActions };
export type StateType = {
count: number;
notification: string | undefined;
};
export type Action = {
type: ActionType;
payload?: string | undefined;
};
interface IContextPropsState {
state: StateType;
}
interface IContextPropsDispatch {
dispatch: Dispatch<Action>;
}
export const ApplicationContextState = React.createContext<IContextPropsState>(
{} as IContextPropsState
);
export const ApplicationContextDispatch =
React.createContext<IContextPropsDispatch>({} as IContextPropsDispatch);
export const useApplicationContextState = (): IContextPropsState => {
return React.useContext(ApplicationContextState);
};
export const useApplicationContextDispatch = (): IContextPropsDispatch => {
return React.useContext(ApplicationContextDispatch);
};
export const InitalState: StateType = {
count: 0,
notification: '',
};
Reducer:
import { StateType, Action, ActionTypes } from './Context';
export const applicationReducer = (
state: StateType,
action: Action
): StateType => {
const { type } = action;
switch (type) {
case ActionTypes.increment:
return { ...state, count: state.count + 1 };
case ActionTypes.decrement:
return { ...state, count: state.count - 1 };
case ActionTypes.notification:
return { ...state, notification: action.payload };
default:
return state;
}
};
Working example here
In the article above this fiddle was presented as an example which I based my attempt on but I dont know where Im going wrong.
Note that the original example of this was done without typescript but in my attempt I am adding typescript into the mix.
The problem is that you are passing a new object into your context providers. It's a classic gotcha. Passing objects as props means you are passing a new object every time which will fail prop reference checks.
Pass dispatch, state directly to the providers i.e. value={dispatch}
https://reactjs.org/docs/context.html#caveats
Caveats
Because context uses reference identity to determine when to re-render, there are some gotchas that could trigger unintentional renders in consumers when a provider’s parent re-renders. For example, the code below will re-render all consumers every time the Provider re-renders because a new object is always created for value:
<MyContext.Provider value={{something: 'something'}}>
#Yuji 'Tomita' Tomita Was 100% on the money.
I had to change my code so that it did not wrap the state and disatch inot an object which in turn made it work as desiered.
Updated code here: https://stackblitz.com/edit/react-ts-7nuhzk?file=Provider.tsx,ButtonDecrement.tsx,App.tsx
I'm relatively new to ReactJS and TypeScript and could use some help with creating a react component that allows me to call methods and realize I probably have a lot of mistakes. Below is my code.
import { SomeExternalPackage } from "#external";
interface MyServiceProviderProps {
isInternal: boolean;
isEnabled: boolean;
}
export const MyServiceProvider: React.FC<MyProviderProps> = (serviceProperty: MyServiceProviderProps) => {
const reportImpressionAsync = (creative: string) => {
if (serviceProperty.isEnabled) {
SomeExternalPackage?.reportImpressionAsync(creative);
}
}
const reportActionAsync = (creative: string, action: SomeExternalPackage.Action) => {
if (serviceProperty.isInternal) {
SomeExternalPackage?.reportActionAsync(creative, action);
}
}
const MyServiceContext = React.createContext(''); // TODO: What do I pass in here?
return (
<MyServiceContext.Provider value={{
reportActionAsync: reportActionAsync,
reportImpressionAsync: reportImpressionAsync
}}>
{serviceProperty} // Is this right?
</MyServiceContext.Provider>
);
};
This is a rough idea of what I have so far. Ideally from a page component, I could use the .Consumer to supply these 2 functions and inside of a page I could call either function.
Based on what I've understood, something like this should work:
import React from 'react'
// faking SomeExternalPackage
type Action = unknown
const SomeExternalPackage = {
reportActionAsync: (creative: string, action: Action) => {},
reportImpressionAsync: (creative: string) => {}
}
interface MyServiceProviderProps {
children: React.ReactNode;
isInternal: boolean;
isEnabled: boolean;
}
export const MyServiceContext = React.createContext({
reportActionAsync: (creative: string, action: Action) => {},
reportImpressionAsync: (creative: string) => {}
})
export const MyServiceProvider: React.FC<MyServiceProviderProps> = (props: MyServiceProviderProps) => {
const reportImpressionAsync = (creative: string) => {
if (props.isEnabled) {
SomeExternalPackage.reportImpressionAsync(creative);
}
}
const reportActionAsync = (creative: string, action: Action) => {
if (props.isInternal) {
SomeExternalPackage.reportActionAsync(creative, action);
}
}
return (
<MyServiceContext.Provider value={{
reportActionAsync: reportActionAsync,
reportImpressionAsync: reportImpressionAsync
}}>
{props.children}
</MyServiceContext.Provider>
);
};
You can then take this MyServiceProvider component and render it somewhere near the top level of your application (like in your App component) so that it wraps all other components and provides these functions.
To use those functions in another component you would do something like this:
import { MyServiceContext } from 'path/to/MyServiceContext'
const MyComponent = () => {
const { reportActionAsync, reportImpressionAsync } = React.useContext(MyServiceContext)
}
I used a map in the data structure, but the components will not update when my data is updated. Why is this?
I use console.log to output 5 pieces of data, but there are only 3 pieces of data on the page, which will not be updated!!!
Component
import React, {ChangeEventHandler, Component} from "react";
import {connect} from 'react-redux';
import {RootState} from "typesafe-actions";
import {getMessage} from "./store/selectors";
import {submitComment} from './store/actions'
const mapDispatchToProps = {
submit: submitComment
};
const mapStateToProps = (state: RootState) => {
return {
article: getMessage(state.article, 1)
}
}
type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps;
type State = {
value: string
}
class Todo extends Component<Props, State> {
readonly state = {value: ''}
public render() {
return (
<div>
<h1>{this.props.article?.title}</h1>
{this.props.article?.comments.map((comment) => <li key={comment.title}>{comment.title}</li>)}
<input type="text" onChange={this.onChange}/>
<button onClick={this.handleSubmit}>submit</button>
</div>
)
}
private handleSubmit = () => {
this.props.submit(this.state.value);
}
private onChange: ChangeEventHandler<HTMLInputElement> = (e) => {
this.setState({value: e.currentTarget.value});
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Todo);
reducer.ts
import {createReducer, PayloadAction} from "typesafe-actions";
import * as actions from './actions';
interface Comment {
title: string
}
interface Article {
title: string
comments: Comment[]
}
interface App {
articles: Map<number, Article>
}
const initState: App = {
articles: new Map<number, Article>([
[1, {title: 'article', comments: [{title: 'comment-1'}, {title: 'comment-2'}]}]
])
}
export const articleReducer = createReducer<App>(initState)
.handleAction(actions.submitComment, (state: App, action: PayloadAction<string, string>) => {
const article = state.articles.get(1)
article?.comments.push({title: action.payload})
console.log(article?.comments);
return {
articles: state.articles
}
});
export default articleReducer;
export type ArticleState = ReturnType<typeof articleReducer>;
actions.ts
import {createAction} from "typesafe-actions";
export const submitComment = createAction("SUBMIT_COMMENT", (title: string) => (title))();
Your articleReduce is modifying an existing state value comments instead of creating a copy in an immutable way:
export const articleReducer = createReducer<App>(initState)
.handleAction(actions.submitComment, (state: App, action: PayloadAction<string, string>) => {
return {
articles: state.articles.map((article, idx) => idx !== 1 ? article : { ...article, comments: [...article.comments, action.payload] })
}
});
If you want to use mutable logic (like .push) in reducers, please see redux toolkit which is the official recommendation to write redux (and works very well with TypeScript) anyways.
you use 'readonly' modifier in
readonly state = {value: ''}
it meaning that 'state' cannot be reassigned
I've been working on a chat-bot app that's written in Typescript and uses Redux-Observable with rxjs. I tried to send a click event from the Shell.tsx component to my redux store which first gets intercepted by rxjs Epic and then gets sent off to redux store. But my redux store's return state does not effect the change that's supposed to be made on a click event, however it does return the expected result on a second click.
This is part of my Shell.tsx that contains the relevant component methods that fires off the click event as an action to the store:
import * as React from 'react';
import { ChatState, FormatState } from './Store';
import { User } from 'botframework-directlinejs';
import { classList } from './Chat';
import { Dispatch, connect } from 'react-redux';
import { Strings } from './Strings';
import { createStore, ChatActions, sendMessage } from './Store';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/observable/merge';
interface Props {
inputText: string,
strings: Strings,
isActive: boolean,
onChangeText: (inputText: string) => void,
sendMessage: (inputText: string) => void
checkActive: (isChatActive: boolean) => void
}
private handleChatClick(isChatActive) {
this.store.dispatch({type: 'Chat_Activate', isChatActive: true})
setTimeout(() => {
this.store.subscribe(() => {
this.isActive = this.store.getState().shell.isChatActive
})
console.log(this.isActive)
}, 3000)
if (this.isActive) {
this.forceUpdate()
}
// this.props.checkActive(true)
}
render() {
//other code
return (
<div className={ className }>
<div className="wc-textbox">
{
console.log('chat rendered')
}
<input
type="text"
className="wc-shellinput"
ref={ input => this.textInput = input }
value={ this.props.inputText }
onChange={ _ => this.props.onChangeText(this.textInput.value) }
onKeyPress={ e => this.onKeyPress(e) }
placeholder={ placeholder }
aria-label={ this.props.inputText ? null : placeholder }
aria-live="polite"
// onFocus={ this.props.handleChatClick}
onClick={() => {
this.handleChatClick(true)
}}
/>
</div>
</div>
);
}
export const Shell = connect(
(state, ownProps) => {
return {
inputText: state.shell.input,
strings: state.format.strings,
isActive: state.shell.isChatActive,
// only used to create helper functions below
locale: state.format.locale,
user: state.connection.user
}
}
, {
// passed down to ShellContainer
onChangeText: (input: string) => ({ type: 'Update_Input', input, source: "text" } as ChatActions),
// only used to create helper functions below
sendMessage
}, (stateProps: any, dispatchProps: any, ownProps: any): Props => ({
// from stateProps
inputText: stateProps.inputText,
strings: stateProps.strings,
isActive: stateProps.isActive,
// from dispatchProps
onChangeText: dispatchProps.onChangeText,
checkActive: dispatchProps.checkActive,
// helper functions
sendMessage: (text: string) => dispatchProps.sendMessage(text, stateProps.user, stateProps.locale),
}), {
withRef: true
}
)(ShellContainer);
This is my the part of my Store.ts code:
export interface ShellState {
sendTyping: boolean
input: string,
isChatActive: boolean,
isPinging: boolean
}
export const setChatToActive = (isChatActive: boolean) => ({
type: 'Chat_Activate',
isChatActive: isChatActive,
} as ChatActions);
export const ping = (isPinging: boolean) => ({
type: 'Is_Pinging',
isPinging: isPinging
} as ChatActions)
export type ShellAction = {
type: 'Update_Input',
input: string
source: "text"
} | {
type: 'Card_Action_Clicked'
} | {
type: 'Set_Send_Typing',
sendTyping: boolean
} | {
type: 'Send_Message',
activity: Activity
} | {
type: 'Chat_Activate',
isChatActive: boolean
} | {
type: 'Is_Pinging',
isPinging: boolean
}
export const shell: Reducer<ShellState> = (
state: ShellState = {
input: '',
sendTyping: false,
isChatActive: false,
isPinging: false
},
action: ShellAction
) => {
console.log(state)
switch (action.type) {
case 'Update_Input':
return {
... state,
input: action.input
};
case 'Send_Message':
return {
... state,
input: ''
};
case 'Chat_Activate':
const newState = {
...state,
isChatActive: action.isChatActive
}
return newState
case 'Set_Send_Typing':
return {
... state,
sendTyping: action.sendTyping
};
case 'Card_Action_Clicked':
return {
... state
};
case 'Is_Pinging':
const newPing = {
... state,
isPinging: action.isPinging
}
return newPing;
default:
return state;
}
}
// 2. Epics
//************************************************************************
//Import modules
import { applyMiddleware } from 'redux';
import { Epic } from 'redux-observable';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/merge';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/mapTo';
import 'rxjs/add/operator/throttleTime';
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/observable/bindCallback';
import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/of';
//************************************************************************
//Asynchronously send messages
const sendMessageEpic: Epic<ChatActions, ChatState> = (action$, store) =>
action$.ofType('Send_Message')
.map(action => {
const state = store.getState();
const clientActivityId = state.history.clientActivityBase + (state.history.clientActivityCounter - 1);
return ({ type: 'Send_Message_Try', clientActivityId } as HistoryAction);
});
const setChatToActiveEpic: Epic<ChatActions, ChatState> = (action$, store) =>
action$.ofType('Chat_Activate')
.mapTo({type: 'Chat_Activate', isChatActive: true} as ChatActions)
.takeUntil(
action$.ofType('Chat_Activate')
)
const pingEpic: Epic<ChatActions, ChatState> = (action$, store) =>
action$.ofType('Is_Pinging')
.mapTo({type: 'Is_Pinging', isPinging: true} as ChatActions)
.takeUntil(
action$.ofType('Is_Pinging')
)
// 3. Now we put it all together into a store with middleware
import { Store, createStore as reduxCreateStore, combineReducers } from 'redux';
import { combineEpics, createEpicMiddleware } from 'redux-observable';
export const createStore = () =>
reduxCreateStore(
combineReducers<ChatState>({
shell,
format,
size,
connection,
history
}),
applyMiddleware(createEpicMiddleware(combineEpics(
updateSelectedActivityEpic,
sendMessageEpic,
trySendMessageEpic,
retrySendMessageEpic,
showTypingEpic,
sendTypingEpic,
setChatToActiveEpic,
pingEpic
)))
);
export type ChatStore = Store<ChatState>;
In a nutshell I want to produce a console log of true when I click on the input element in my Shell.tsx. But the output is always false when I click on the input for the first time, works when I click it again.
I don't see anything immediately wrong in your code that would cause state to not be changed the first time that the action is dispatched. The logging is a little confusing though, so it might be causing you to think the code is behaving differently than it is?
From your screenshot, I can see that the first console.log statement is from store.ts line 84 (if you look all the way to the right, you can see that), and the second console.log is coming from the component.
In your store.ts file, you have a console.log statement at the top of the reducer. Because this logging is at the top of the reducer, it will always display the previous state, not the updated state.
export const shell: Reducer<ShellState> = (
state: ShellState = {
input: '',
sendTyping: false,
isChatActive: false,
isPinging: false
},
action: ShellAction
) => {
console.log(state)
Another thing that may be confusing you is that you are listening to store changes AFTER you've changed the store.
// this updates the store
this.store.dispatch({type: 'Chat_Activate', isChatActive: true})
// 3 seconds later, you're listening for store changes, but it's already changed
setTimeout(() => {
this.store.subscribe(() => {
this.isActive = this.store.getState().shell.isChatActive
})
// then you console.log this.isActive which might be false because that's the initial state in the reducer
console.log(this.isActive)
}, 3000)
You should subscribe to the store BEFORE you dispatch the action, if you want to see it change.
this.store.subscribe(() => {
this.isActive = this.store.getState().shell.isChatActive;
});
this.store.dispatch({type: 'Chat_Activate', isChatActive: true});
Alternatively, you can use Connected components from React-Redux. They will listen for store changes and update components automatically for you, so that you don't have to subscribe to the store for changes yourself.
React Redux github: https://github.com/reactjs/react-redux
Intro Blog Post: https://www.sohamkamani.com/blog/2017/03/31/react-redux-connect-explained/
If you want to see the stores value update immediately with console.log, you could do something like
private handleChatClick(isChatActive) {
console.log(`before click/chat active event: ${this.store.getState().shell.isChatActive}`);
this.store.dispatch({type: 'Chat_Activate', isChatActive: true})
console.log(`after click/chat active event: ${this.store.getState().shell.isChatActive}`);
}