Redux -- can two containers share state? - reactjs

I am super new to both React and Redux. I am at the point where I am trying to bring several containers into a parent container, a form.
I started by creating a Select component that is reused by using Redux.
The Select component has one set of actions. But there are unique reducers for each of the instances of the Select component. All come together by creating a container for each instance of the Select. (See code below).
The Select containers work perfectly. And the coexist on the same page without a problem.
So now I want to "wire them" into a form. The form needs to know what value the user has selected. So I thought I could do that by mapping properties for the form to the appropriate bits of state associated with the Select containers. That works for initialization but when I select an item from one of the Select containers the new value is not reflected in the form container.
Can this be done?
/reducers/base_reducers/SingleSelect.js
import { FETCH_ITEMS, ADD_ITEM, SET_VALUE, SHOW_LOADING, SHOW_ERROR } from "../../constants";
// Defines STATE for the SingleSelect component and initial defaults. Any key with a value of 'DEFINE'
// must be INITIALIZED in the model reducer. Remaining items can also be modified or left as initialized here.
export const base_reducer_single_select_state = {
namespace: 'DEFINE',
componentId: 'DEFINE',
items: ['DEFINE',],
defaultValue: 'DEFINE',
selectItem: '0',
allowAdd: true,
validationRegEx: /^[a-zA-Z ]*$/,
regExDescription: 'letters and spaces.',
errorMessage: '',
isError: false,
isLoading: false,
apiRoute: 'DEFINE',
};
// Processes state changes from actions for the Simple Select component.
export function base_reducer_single_select(state, action) {
switch (action.type) {
case `${state.namespace}/${FETCH_ITEMS}`:
action.items.unshift(state.defaultValue);
return {...state, "items": action.items};
case `${state.namespace}/${ADD_ITEM}`:
return {...state, "selectItem": action.itemValue};
case `${state.namespace}/${SET_VALUE}`:
return {...state, "selectItem": action.newValue};
case `${state.namespace}/${SHOW_ERROR}`:
return {...state,
"isError": action.trueFalse,
"errorMessage": action.message};
case `${state.namespace}/${SHOW_LOADING}`:
return {...state, "isLoading": action.value};
default:
return state;
}
}
And here are a couple of the reducers that implement the generic SimpleSelect reducer:
/reducers/City.js
import { base_reducer_single_select, base_reducer_single_select_state } from './base_reducers/SingleSelect';
import { getNamespace } from '../helpers';
const defaultValue = {id: 0, name: "- Select city -"};
const initialState = {
...base_reducer_single_select_state,
namespace: getNamespace(),
componentId: 'City',
items: [
defaultValue,
],
defaultValue: defaultValue,
apiRoute: '/api/cities/',
};
export default function city(state=initialState, action) {
return base_reducer_single_select(state, action);
}
/reducers/State.js
import { base_reducer_single_select, base_reducer_single_select_state } from './base_reducers/SingleSelect';
import { getNamespace } from '../helpers';
const defaultValue = {id: 0, name: '- Select state -'};
const initialState = {
...base_reducer_single_select_state,
namespace: getNamespace(),
componentId: 'State',
items: [
defaultValue,
],
defaultValue: defaultValue,
apiRoute: '/api/states/',
};
export default function state_name(state=initialState, action) {
return base_reducer_single_select(state, action);
}
Their containers:
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import SingleSelectComponent from '../components/SingleSelectComponent';
import * as CityActions from '../actions/SingleSelect';
const mapStateToProps = state => {
return {
namespace: state.city.namespace,
componentId: state.city.componentId,
items: state.city.items,
selectItem: state.city.selectItem,
allowAdd: state.city.allowAdd,
validationRegEx: state.city.validationRegEx,
regExDescription: state.city.regExDescription,
errorMessage: state.city.errorMessage,
isError: state.city.isError,
isLoading: state.city.isLoading,
apiRoute: state.city.apiRoute
};
};
function mapDispatchToProps(dispatch) {
return {actions: bindActionCreators(CityActions, dispatch)};
}
const CityContainer = connect(
mapStateToProps,
mapDispatchToProps
)(SingleSelectComponent);
export default CityContainer;
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import SingleSelectComponent from '../components/SingleSelectComponent';
import * as StateActions from '../actions/SingleSelect';
const mapStateToProps = state => {
return {
namespace: state.state_name.namespace,
componentId: state.state_name.componentId,
items: state.state_name.items,
selectItem: state.state_name.selectItem,
allowAdd: state.state_name.allowAdd,
validationRegEx: state.state_name.validationRegEx,
regExDescription: state.state_name.regExDescription,
errorMessage: state.state_name.errorMessage,
isError: state.state_name.isError,
isLoading: state.state_name.isLoading,
apiRoute: state.state_name.apiRoute
};
};
function mapDispatchToProps(dispatch) {
return {actions: bindActionCreators(StateActions, dispatch)};
}
const StateNameContainer = connect(
mapStateToProps,
mapDispatchToProps
)(SingleSelectComponent);
export default StateNameContainer;
And the "form" container (which is not a form yet as I'm just doing the initial testing)
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import CompanyInfoComponent from '../components/CompanyInfoComponent';
import * as CompanyInfoActions from '../actions/CompanyInfo';
const mapStateToProps = state => {
return {
namespace: state.company_info.namespace,
componentId: state.company_info.componentId,
childCityValue: state.city.selectItem,
childStateValue: state.state_name.selectItem,
childCountryValue: state.country.selectItem,
validationRegEx: state.company_info.validationRegEx,
regExDescription: state.company_info.regExDescription,
errorMessage: state.company_info.errorMessage,
isError: state.company_info.isError,
isLoading: state.company_info.isLoading,
apiRoute: state.company_info.apiRoute
};
};
function mapDispatchToProps(dispatch) {
return {actions: bindActionCreators(CompanyInfoActions, dispatch)};
}
const CompanyInfoContainer = connect(
mapStateToProps,
mapDispatchToProps
)(CompanyInfoComponent);
export default CompanyInfoContainer;
Again, the initial values are showing up correctly. But when state changes, meaning for instance, when I select a City, the state is changing when I examine state associated with the city (state.city.selectItem) but that is not being reflected in the CompanyInfo container.
Thanks in advance for your time reading all that code and any insight/help you can provide.

Related

React useReducer not picking up new dispatched values from other component

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

How do i manage custom props like 'isInnerPage' within router reducer?

I use connected-react-router and have some types of pages: main, inner, auth, admin. Depends of current page type I render certain components. Now it works in this way: there are 4 props in my config reducer with bool values: isMainPage, isInnerPage, isAdminPage, isAuthPage, and only one of them could be true in a time. They changes any time I change location (by executing certain action in componentDidMount). So, I want to deal with connected-react-router and, if possible, pass these props from config reducer to the router. Then, if possible, I want to execute an action that will define current page type and set it and then I would get this val in components. Is it all possible? Sorry for this explanation - I'm just studying.
I would provide some code but I don't know which part could be helpful - ask for one, please
config reducer:
import {
SET_VIEW_MODE,
SET_AUTH_PAGE,
SET_MAIN_PAGE,
SET_INNER_PAGE,
SET_ADMIN_PAGE,
...
} from '../constants';
...
case SET_AUTH_PAGE:
return {
...state,
isAuthPage: true,
isMainPage: false,
isInnerPage: false,
isAdminPage: false
};
case SET_MAIN_PAGE:
return {
...state,
isAuthPage: false,
isMainPage: true,
isInnerPage: false,
isAdminPage: false
};
case SET_INNER_PAGE:
return {
...state,
isAuthPage: false,
isMainPage: false,
isInnerPage: true,
isAdminPage: false
};
case SET_ADMIN_PAGE:
return {
...state,
isAuthPage: false,
isMainPage: false,
isInnerPage: false,
isAdminPage: true
};
config actions:
...imports;
export const actionSetViewMode = () => ({ type: SET_VIEW_MODE });
export const actionSetAuthPage = () => ({ type: SET_AUTH_PAGE });
export const actionSetMainPage = () => ({ type: SET_MAIN_PAGE });
export const actionSetInnerPage = () => ({ type: SET_INNER_PAGE });
expample of component that sets any type:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actionSetMainPage } from '../actions/configActions';
...
class MainPage extends Component {
componentDidMount() {
this.props.actionSetMainPage();
}
render() {
return (
<>
...
</>
)
}
}
export default connect(null, {
actionSetMainPage,
})(MainPage);
example of component that renders any depends of page type:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import Subaction from '../components/header/Subaction';
import Back from '../components/header/Back';
import Menu from '../components/header/Menu';
import Title from '../components/header/Title';
import Logo from '../components/header/Logo';
import Notification from '../components/header/Notification';
class Header extends Component {
render() {
const { isAdaptive, isAuthPage, isMainPage, isInnerPage, title } = this.props.config;
return (
!isAuthPage &&
<header>
<div className="h-inner">
...
{
isMainPage &&
<Logo></Logo>
}
<Notification></Notification>
...
</div>
</header>
)
}
}
export default connect(state => ({
config: state.config
}), {})(Header);

Adding item to array in child object of a child object via reducer

I apologize if my terminology is incorrect. I am very new to reactjs and react-redux (couple days).
The challenge I am trying to overcome is adding an item in store.order.cart.items. My store structure is as such:
{
order:
cart:
items: [
{name: "product a"},
{name: "product b"}
]
}
My reducer is as follows:
import ApplicationState from '../application-state';
import { ADD_PRODUCT } from "../constants/action-types";
const initialState = ApplicationState;
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_PRODUCT:
return {
...state, order: {
...state.order, cart: {
...state.cart, items: [
...state.items, action.payload
]
}
}
};
default:
return state;
}
};
export default rootReducer;
My action for ADD_PRODUCT is as follows:
import { ADD_PRODUCT } from "../constants/action-types"
export const addProduct = product => ({ type: ADD_PRODUCT, payload: product });
And my action type constants (constants/action-types):
export const ADD_PRODUCT = "ADD_PRODUCT";
When I dispatch my addProduct action (testing this via the browser console: store.dispatch( addProduct({ name: "product c" }) )
I get the following error:
app.js:36995 Uncaught TypeError: Cannot convert undefined or null to object
at Function.from (/native)
at _toConsumableArray (app.js:36995)
at rootReducer (app.js:37009)
at Object.dispatch (app.js:13839)
at <anonymous>:1:7
What am I doing wrong? What is the correct way to get the new item added to the order.cart.items array?
items: [
...state.items, action.payload
]
In this excerpt above, you have attempted to spread ...state.items into the items array, but items is actually at state.order.cart.items;
Similarly:
cart: {
...state.cart,
}
cart is actually at state.order.cart
Deeply nested situations like this are why it's best to flatten your state where possible and use combineReducers(). Alternately you could look at an immutability helper to help with immutably setting deeply nested state

State doesnt get reset on route change - React-redux-router

Hi I am creating a Server side react app. I have multiple routes using the same components and reducers. One specific reducer is an ItemsPerPage dropdown. I am getting the value from the reducer and passing it as a payload to a post request to a database to fetch that many results.
In one page, I get 50 results, and when I navigate to the other page using the same reducer, the state value should be 10 but rather its 50. How do I reset the state when going to another page?
I am using { LOCATION_CHANGE } from 'react-router-redux'
routerReducer.js:
import { LOCATION_CHANGE } from 'react-router-redux';
const initialState = {
location: '',
};
export const routeReducer = (state = initialState, action) => {
switch (action.type) {
case LOCATION_CHANGE:
return {
...state,
...action.payload,
};
default:
return state;
}
};
ItemsPerPageDropdown:
import React, {PropTypes, Component} from 'react';
import _ from 'lodash';
import { connect } from 'react-redux';
import { changeItemsPerPage } from '../../actions/index';
class ItemsPerPage extends Component {
handleChange = (event) => {
this.props.changeItemsPerPage(event.target.value)
};
render() {
const itemsPerPage = [10, 20, 50];
return (
<div className={'table-item-count-container'}>
<label className={'filter-label items-by-page-label'}>Items Per Page:</label>
<select id="items-per-paghe"
className="form-control items-by-page-select"
onChange={this.handleChange}
>
{_.map(itemsPerPage, (item, index) => <option key={index}>{item}</option>)}
</select>
</div>
)
}
}
export default connect(null, {changeItemsPerPage})(ItemsPerPage);
ItemsPerPageReducer:
import * as ACTION_TYPES from '../consts/action_types';
const initialState = {
items: 10,
};
export const itemsPerPageReducer = (state = initialState, action) => {
switch (action.type) {
case ACTION_TYPES.CHANGE_ITEMS_PER_PAGE:
return {
...state,
items: action.data,
};
default:
return state;
}
};
Main page using this component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom'
import { withRouter } from 'react-router'
import { bindActionCreators } from 'redux';
import _ from 'lodash';
import Moment from 'react-moment';
import Header from '../../components/Header/Header';
import DataTable from '../../components/DataTable/DataTable';
import InstructionList from '../../components/InstructionList/InstructionList';
import { getData, postData } from '../../actions';
import { columns } from './Report1Columns';
import * as instructions from '../../consts/instructionText';
class Report1 extends Component {
params = {
userId: this.props.corpId,
filteredAppID: '',
envClassification: '',
Status: '',
startRow: 0 + this.props.activePage, //1
numRows: this.props.itemsPerPage,
sortCol: 'application_name',
sortDir: 'asc',
};
loadPage = () => {
if(this.props.postData) {
this.props.postData('https://localhost:3001/reports/report1/', this.params);
}
};
componentDidMount = () => {
this.props.postData('https://localhost:3001/reports/report1/', this.params);
};
componentWillReceiveProps = (nextProps) => {
if (nextProps.itemsPerPage !== this.props.itemsPerPage) {
this.params.numRows = nextProps.itemsPerPage;
this.loadPage();
}
if(nextProps.activePage !== this.props.activePage) {
this.params.startRow = ((nextProps.activePage - 1) * this.props.itemsPerPage) +1;
this.loadPage();
}
if(nextProps.searchTerm !== this.props.searchTerm) {
this.params.filteredAppID = nextProps.searchTerm;
this.loadPage();
}
if(nextProps.envClassification !== this.props.envClassification) {
this.params.envClassification = nextProps.envClassification === 'All' ? '' : nextProps.envClassification;
this.loadPage();
}
if(nextProps.watchtowerStatus !== this.props.Status) {
this.params.watchtowerStatus= nextProps.watchtowerStatus=== 'Manage & Analyze' ? '' : nextProps.watchtowerStatus;
this.loadPage();
}
};
render() {
return (
<div>
<Header title={ 'Report 1' } />
<InstructionList instructions={ instructions.Report1 } />
{this.props.data &&
<DataTable
keyField={ 'Report1' }
columns={ columns }
paginatedData={ this.props.data }
totalRows={ this.props.totalRows }
placeholder={ 'ID/NAME' }
showStatus={true}
/>}
</div>
);
}
}
const mapStateToProps = state => ({
/**
* The date to be passed to the table
*/
data: _.get(state.isFetchingPost, 'data.rows'),
/**
* Total Rows count of data
*/
totalRows: state.isFetchingPost.data.total_rows,
/**
* Items Per Page
*/
itemsPerPage: state.itemsPerPageReducer.items,
/**
* Item which is searched
*/
searchTerm: state.searchReducer.searchTerm,
/**
* The current active page for pagination
*/
activePage: state.changeActivePageReducer.activePage,
/**
* The value of the dropdown selected in Environment Classification
*/
envClassification: state.envClassificationReducer.envClassification,
/**
* The value of the dropdown selected in Status
*/
watchtowerStatus: state.watchTowerStatusReducer.watchTowerStatus
});
const mapDispatchToProps = dispatch => bindActionCreators({ getData, postData }, dispatch);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Report1));
If you can see in the images below, I navigate between the routes and state still remains same instead of taking initial state.
Add a case for LOCATION_CHANGE to your ItemsPerPageReducer to reset the count when the location changes:
import * as ACTION_TYPES from '../consts/action_types';
import { LOCATION_CHANGE } from 'react-router-redux';
const initialState = {
items: 10,
};
export const itemsPerPageReducer = (state = initialState, action) => {
switch (action.type) {
case ACTION_TYPES.CHANGE_ITEMS_PER_PAGE:
return {
...state,
items: action.data,
};
case LOCATION_CHANGE:
return {
...state,
items: 10,
};
default:
return state;
}
}
};
If you only want it to reset on certain location changes, you can check action.payload to test if it is a route you actually want to reset on.

Redux-Observable and Rxjs not capturing the first event but captures the second event

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}`);
}

Resources