Adding events to ReactBigCalendar using redux - reactjs

I'm currently working on a booking app, and in that regard I need to fetch some data from an API. I'm using react + redux to do this, but I can't seem to get this element BigCalendar to update when the state is updated. BigCalendar has to have some kind of object for it's init process.
BookingReducer.js
import { fetchBookingStart, fetchBookingsSuccess, fetchBookingsError } from '../types.js';
const initialState = {
fetching: false,
fetched: false,
events: [],
error: null
}
export default function (state = initialState, action) {
switch (action.type) {
case fetchBookingStart:
return {
...state,
fetching: true,
fetched: false
}
case fetchBookingsSuccess:
return {
...state,
fetching: false,
fetched: true,
events: action.payload.bookings.length < 0 ? [] : action.payload.bookings.map(B => {
return {
id:22,
title: "testTitle",
description: B.description,
start: B.dateFrom,
end: B.dateTo,
room: B.room,
user: B.user
}
})
}
case fetchBookingsError:
return {
...state,
fetching: false,
error: action.payload.error
}
default:
return state;
}
}
bookingAction.js
import axios from "axios";
import { fetchBookingStart, fetchBookingsSuccess, fetchBookingsError } from '../types.js';
const apiUrl = "http://localhost/api/booking"; //CHANGE FOR PROD!
export const getBookings = () => dispatch => {
let allUrl = apiUrl + "/find/";
dispatch({type: fetchBookingStart});
axios.get(allUrl).then(response => {
console.log(response.data);
dispatch({
type: fetchBookingsSuccess,
payload: response.data
})
}).catch(error => {
dispatch({
type: fetchBookingsError,
payload: error
});
});
}
App.js
import React, { Component } from "react";
import { connect } from 'react-redux';
import { getBookings } from './components/redux/actions/bookingActions';
import Link from "react-router-dom/Link";
import BigCalendar from 'react-big-calendar';
import moment from 'moment';
import 'react-big-calendar/lib/css/react-big-calendar.css';
import "./style.css";
import 'moment/locale/da';
import BookingDialog from './components/booking_create_dialog/BookingDialog';
import Dialog from "material-ui/Dialog/Dialog";
import FlatButton from "material-ui/FlatButton/FlatButton";
let isDialogOpen = false;
moment.locale('da');
BigCalendar.momentLocalizer(moment);
function eventStyleGetter(event, start, end, isSelected) {
let style = { backgroundColor: "" }
switch (event.room) {
case "sal":
style.borderColor = "#781B7F";
style.backgroundColor = "#781B7F";
break;
case "cafe":
style.borderColor = "#067F3D";
style.backgroundColor = "#067F3D";
break;
default:
break;
}
return { style: style };
}
class App extends Component {
componentWillMount() {
this.props.getBookings();
}
componentDidUpdate() {
if (this.props.fetched === true && this.props.fetching === false) {
this.refs.BigCalendar.forceUpdate();
}
}
render() {
return (
<div className="MainContainer">
{/*<Link to="/login"><FlatButton>Login</FlatButton></Link>*/}
<div className="CalendarContainer">
<BigCalendar
ref="BigCalendar"
selectable
className="Calendar"
events={this.props.events}
defaultView="week"
defaultDate={new Date()}
step={60}
eventPropGetter={eventStyleGetter}
/>
</div>
<div className="TestContainer">
<button onClick={() => {
this.props.events.push({
id: 55,
title: "test event",
allDay: true,
start: new Date(),
end: new Date()
}); console.log(this.props.events)
}}> bookigns </button>
{this.props.events.map(E => <h1> {E.room} </h1>)}
</div>
</div>
)
}
}
const mapStateToProps = state => ({
events: state.bookings.events,
fetched: state.bookings.fetched,
fetching: state.bookings.fetching
})
export default connect(mapStateToProps, { getBookings })(App);
Please point out any mistakes that can help me along the right way of doing this.

I was having a similar problem with my react-redux setup and realized my events array of objects wasn't formatted correctly. Other than checking this, I'd also check to see if BigCalendar is getting rendered with anything from this.props.events in the first place. Every time the events prop is changed for BigCalendar it updates itself.

Related

this.setState not updating state

For some reason my this.state call is not updating the state in my roleClicked function. I can't seem to figure out what the problem is. Here is the code:
import React, { Component } from 'react'
import { Redirect } from 'react-router-dom';
import instance from '../../../config/axios';
import ls from 'local-storage';
import Sidenav from '../../layout/Sidenav'
import isLoggedIn from '../../../helpers/isLoggedIn';
import redirectToLogin from '../../../helpers/redirectToLogin';
import apiErrorHandler from '../../../helpers/apiErrorHandler';
import RolesHeader from './RolesHeader';
import '../../../App.css'
import { Table, Pagination, Input, Alert } from 'antd';
export default class ViewRoles extends Component {
state = {
loggedIn: true,
roleSelected: false,
roleSelectedId: null,
rolesTable: {
columns: null,
dataSource: null,
currentPageNumber: null,
currentPageSize: null,
total: null,
},
filters: {
roleName: null
},
displays: {
createRoleView: false,
roleCreatedAlert: false,
},
errors: null
}
componentDidMount = () => {
//here i make AJAX calls to set rolesTables
}
roleClicked = (record) => {
console.log("clicked")
this.setState({
roleSelected: true,
roleSelectedId: record.key
})
}
render() {
if(this.state.roleSelected) {
const roleUrl = "/roles/" + this.state.roleSelectedId
return <Redirect to={roleUrl}/>
}
return (
<React.Fragment>
<Sidenav activeLink="roles"/>
<Table
className="border"
columns={this.state.rolesTable.columns}
dataSource={this.state.rolesTable.dataSource}
pagination={false}
onRow={(record) => {
return {
onClick: () => this.roleClicked(record)
}}}
/>
<Pagination
className="m-3"
current={this.state.rolesTable.currentPageNumber}
pageSize={this.state.rolesTable.currentPageSize}
total={this.state.rolesTable.total}
onChange={this.loadRolesTable}
/>
</React.Fragment>)
}
}
The function runs as expected but the state is not updated. Any idea what the problem could be?
Everywhere else I use this.setState it works. The full code can be found on this link.
I updated roleClicked to this and it works. I still have no idea why the initial code didn't work.
viewRole = async (record) => {
var currentState = this.state;
currentState.roleSelected = true;
currentState.roleSelectedId = record.key;
await this.setState(currentState);
console.log(this.state)
}
Just changing it to Async didn't work. Only assigning the state to currentState and then updating currentState and using it in setState worked. And now it works both synchronously and asynchronously.
try something like this
this.setState(function(state, props) {
return {
...state,
roleSelected: true,
roleSelectedId: record.key
};
});

How to use Loader on async multiple request in Redux and React.js

I'm trying to set Loader when data is not fetched yet. This scenario would be easy if the data would be uploaded only ones (logic here: set a flag is isFetching to true, when receiving from redux set it to false). But my scenario is a bit different. I'd like to get my data multiple times to update my Calendar component. All is done thru redux with axios package.
It looks like that:
My reducer adds isFetching flag when my axios request is done(the store is updated):
import { ACTIVE_MONTH } from "../actions/types";
export default function(state = null, action){
switch(action.type){
case ACTIVE_MONTH:
return Object.assign({}, state, {
isFetching: false,
fullyBooked: action.payload
})
default:
return state;
}
}
And the component looks like that:
import React, { Component } from 'react';
import Calendar from 'react-calendar';
import ChooseHour from './ChooseHour';
import { connect } from 'react-redux';
import * as actions from '../actions';
class Calendario extends Component {
state = { showHours: false, disabledDates: null}
componentDidMount() {
const { chosenRoom } = this.props;
const date = new Date();
const reqMonth = date.getMonth() + 1;
const reqYear = date.getFullYear();
this.props.activeMonthYearToPass({reqMonth, reqYear, chosenRoom});
}
onChange = date => this.setState({ date }, () => {
const { chosenRoom, isBirthday } = this.props;
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const fullDate = `${year}/${month}/${day}`;
const roomAndDayObj = {fullDate, chosenRoom, isBirthday};
this.props.sendRoomAndDay(roomAndDayObj);
}
);
onClickDay(e) {
const { chosenRoom } = this.props;
!chosenRoom ? this.setState({ errorMsg: "Wybierz pokój", showHours: false}) :
this.setState({ showHours: true, errorMsg:'' });
}
passActiveDate(activeDate) {
const { chosenRoom } = this.props;
const reqMonth = activeDate.getMonth() + 1;
const reqYear = activeDate.getFullYear();
this.setState({ pending: true},
() => this.props.activeMonthYearToPass({reqMonth, reqYear, chosenRoom})
);
this.props.passDateDetails({reqMonth, reqYear});
}
render() {
const { fullyBookedDays, isBirthday } = this.props;
const { errorMsg, pending } = this.state;
return (
<div>
<div className="calendarsCont">
<Calendar
onChange={this.onChange}
onClickDay={(e) => this.onClickDay(e)}
onActiveDateChange={({ activeStartDate }) => this.passActiveDate(activeStartDate)}
value={this.state.date}
locale="pl-PL"
tileDisabled={({date, view}) =>
(view === 'month') &&
fullyBookedDays && fullyBookedDays.fullyBooked.some(item =>
date.getFullYear() === new Date(item).getFullYear() &&
date.getMonth() === new Date(item).getMonth() -1 &&
date.getDate() === new Date(item).getDate()
)}
/>
}
</div>
<p style={{color: 'red'}}>{errorMsg}</p>
<div>
{this.state.showHours ?
<ChooseHour chosenDay={this.state.date} chosenRoom={this.props.chosenRoom} isBirthday={isBirthday}/> :
null}
</div>
</div>
)
}
}
function mapStateToProps({fullyBookedDays}){
return {
fullyBookedDays,
}
}
export default connect (mapStateToProps, actions)(Calendario);
So the new values will come many times from axios request.
What kind of strategy do you use in that case?
THANK YOU!
Whenever there is multiple fetching requests, or even multiple actions that indicates something async is happening and needs to be stored in a part of the state, I use a counter :
export default function(state = {fetchCount: 0}, action){
switch(action.type){
case FETCHING_THING:
return Object.assign({}, state, {
fetchCount: state.fetchCount + 1
})
case FETCHING_THING_DONE:
return Object.assign({}, state, {
fetchCount: state.fetchCount - 1,
fullyBooked: action.payload
}
default:
return state;
}
}
Then you can just check fetchCount > 0 in your mapstatetoprops.
function mapStateToProps({fullyBookedDays, fetchCount}){
return {
fullyBookedDays,
isLoading: fetchCount > 0
}
}
Please take below as an example , Redux-thunk style action is used for wrap multiple axios requests and dispatch them all.
//axios call2
function getData1() {
return axios.get('/data1');
}
//axios call2
function getData2() {
return axios.get('/data2');
}
//redux-thunk action creator
function getFullData() {
return (dispatch, getState) => {
axios.all([getData1(), getData2()])
.then(axios.spread(function (acct, perms) {
//call normal action creator
dispatch(fetchData1())
dispatch(fetchData2())
}));
};
}
//normal actioncreator
function fetchData1(data)
{
return {type: "FETCH_DATA1", payload: data}
}
//normal actioncreator
function fetchData2(data)
{
return {type: "FETCH_DATA2", payload: data}
}
//reducer1:
function reducer1 (state = defaultedState ,action){
return Object.assign({},{...state, data: action.payload, isFetching: false} )
}
//reducer2:
function reducer2 (state = defaultedState ,action){
return Object.assign({},{...state, data: action.payload, isFetching: false} )
}
//component:
mapStateToProps = function(state){
return {
data1: state.data1.data,
data2: state.data2.data,
isFetching1: state.data1.isFetching,
isFetching2: state.data2.isFetching
}
}
import React, { Component } from "react";
class MyComponent extends Component{
render(){
return (!data1 && isFetching1) || (!data2 && isFetching2) ? <Loading> : <DataComponent>
}
}
connect(mapStateToProps)(MyComponent)

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

Lot of repetition in React component

I have a rather large React component that manages the display of a detail for a job on my site.
There are a few things that I would like to do smarter
The component has a few options for opening Dialogs. For each dialog I have a separate Open and Close function. For example handleImageGridShow and handleImageGridClose. Is there any way to be more concise around this?
I have many presentational components (e.g. ViewJobDetails) that shows details about the job. My issue is that I have to pass them down into each Component as a prop and I'm passing the same props over and over again
As I'm loading my data from firebase I often have to do similar checks to see if the data exists before I render the component (e.g.this.state.selectedImageGrid && <ImageGridDialog />). Is there any more clever way of going about this?
import React, { Component } from 'react';
import { withStyles } from 'material-ui/styles';
import ViewJobAttachment from "../../components/jobs/viewJobAttachment";
import ViewJobDetails from "../../components/jobs/viewJob/viewJobDetails";
import ViewJobActions from "../../components/jobs/viewJob/viewJobActions";
import ViewCompanyDetails from "../../components/jobs/viewJob/viewCompanyDetails";
import ViewClientsDetails from "../../components/jobs/viewJob/viewClientsDetails";
import ViewProductsDetails from "../../components/jobs/viewJob/viewProductsDetails";
import ViewAttachmentDetails from "../../components/jobs/viewJob/viewAttachmentDetails";
import ViewEventLogDetails from "../../components/jobs/viewJob/viewEventLogDetails";
import ViewSummaryDetails from "../../components/jobs/viewJob/viewSummary";
import {FirebaseList} from "../../utils/firebase/firebaseList";
import SimpleSnackbar from "../../components/shared/snackbar";
import {calculateTotalPerProduct} from "../../utils/jobsService";
import BasicDialog from "../../components/shared/dialog";
import ImageGrid from "../../components/shared/imageGrid";
import Spinner from "../../components/shared/spinner";
import ViewPinnedImageDialog from "../../components/jobs/viewEntry/viewPinnedImage";
import {
Redirect
} from 'react-router-dom';
const styles = theme => ({
wrapper: {
marginBottom: theme.spacing.unit*2
},
rightElement: {
float: 'right'
}
});
const ImageGridDialog = (props) => {
return (
<BasicDialog open={!!props.selectedImageGrid}
handleRequestClose={props.handleRequestClose}
fullScreen={props.fullScreen}
title={props.title}
>
<ImageGrid selectedUploads={props.selectedImageGrid}
handleClickOpen={props.handleClickOpen}/>
</BasicDialog>
)
};
class ViewJob extends Component {
constructor() {
super();
this.state = {
currentJob: null,
entries: [],
promiseResolved: false,
attachmentDialogOpen: false,
openAttachment: null,
selectedImageGrid: false,
selectedPinnedImage: false,
showSnackbar: false,
snackbarMsg: '',
markedImageLoaded: false,
loading: true,
redirect: false
};
this.firebase = new FirebaseList('jobs');
this.handleJobStatusChange = this.handleJobStatusChange.bind(this);
this.handleImageGridShow = this.handleImageGridShow.bind(this);
this.handleImageGridClose = this.handleImageGridClose.bind(this);
this.handlePinnedImageClose = this.handlePinnedImageClose.bind(this);
this.handlePinnedImageShow = this.handlePinnedImageShow.bind(this);
this.handleMarkedImageLoaded = this.handleMarkedImageLoaded.bind(this);
this.handleRemove = this.handleRemove.bind(this);
this.pushLiveToClient = this.pushLiveToClient.bind(this);
}
componentDidMount() {
this.firebase.db().ref(`jobs/${this.props.id}`).on('value', (snap) => {
const job = {
id: snap.key,
...snap.val()
};
this.setState({
currentJob: job,
loading: false
})
});
const previousEntries = this.state.entries;
this.firebase.db().ref(`entries/${this.props.id}`).on('child_added', snap => {
previousEntries.push({
id: snap.key,
...snap.val()
});
this.setState({
entries: previousEntries
})
});
}
handleRemove() {
this.firebase.remove(this.props.id)
.then(() => {
this.setState({redirect: true})
})
};
pushLiveToClient() {
const updatedJob = {
...this.state.currentJob,
'lastPushedToClient': Date.now()
};
this.firebase.update(this.state.currentJob.id, updatedJob)
.then(() => this.handleSnackbarShow("Job pushed live to client"))
}
handleJobStatusChange() {
const newState = !this.state.currentJob.completed;
const updatedJob = {
...this.state.currentJob,
'completed': newState
};
this.firebase.update(this.state.currentJob.id, updatedJob)
}
handleSnackbarShow = (msg) => {
this.setState({
showSnackbar: true,
snackbarMsg: msg
});
};
handleSnackbarClose= (event, reason) => {
if (reason === 'clickaway') {
return;
}
this.setState({ showSnackbar: false });
};
handleAttachmentDialogClose =() => {
this.setState({attachmentDialogOpen: false})
};
handleClickOpen = (file) => {
this.setState({
attachmentDialogOpen: true,
openAttachment: file
});
};
handleImageGridShow(imageGrid) {
this.setState({selectedImageGrid: imageGrid})
}
handleImageGridClose() {
this.setState({selectedImageGrid: false})
}
handlePinnedImageShow(pinnedImage) {
this.setState({selectedPinnedImage: pinnedImage})
}
handlePinnedImageClose() {
this.setState({selectedPinnedImage: false})
}
handleMarkedImageLoaded() {
this.setState({markedImageLoaded: true})
}
render() {
const {classes} = this.props;
let {_, costPerItem} = calculateTotalPerProduct(this.state.entries);
if (this.state.redirect) {
return <Redirect to='/jobs' push/>
} else {
if (this.state.loading) {
return <Spinner/>
} else {
return (
<div className={styles.wrapper}>
{this.state.currentJob &&
<div>
<ViewJobActions currentJob={this.state.currentJob}
handleJobStatusChange={this.handleJobStatusChange}
pushLiveToClient={this.pushLiveToClient}
/>
<ViewJobDetails currentJob={this.state.currentJob}/>
<ViewCompanyDetails currentJob={this.state.currentJob}/>
<ViewClientsDetails currentJob={this.state.currentJob}/>
<ViewProductsDetails currentJob={this.state.currentJob}/>
{this.state.currentJob.selectedUploads && this.state.currentJob.selectedUploads.length > 0
? <ViewAttachmentDetails currentJob={this.state.currentJob} handleClickOpen={this.handleClickOpen}/>
: null}
<ViewEventLogDetails jobId={this.state.currentJob.jobId}
jobKey={this.state.currentJob.id}
entries={this.state.entries}
handlePinnedImageShow={this.handlePinnedImageShow}
handleImageGridShow={this.handleImageGridShow}/>
<ViewSummaryDetails stats={costPerItem}/>
<ViewJobAttachment open={this.state.attachmentDialogOpen}
handleRequestClose={this.handleAttachmentDialogClose}
attachment={this.state.openAttachment}
/>
{this.state.selectedImageGrid &&
<ImageGridDialog selectedImageGrid={this.state.selectedImageGrid}
handleRequestClose={this.handleImageGridClose}
handleClickOpen={this.handleClickOpen}
title="Pictures for job"
fullScreen={false}/>}
{this.state.selectedPinnedImage &&
<ViewPinnedImageDialog attachment={this.state.selectedPinnedImage}
open={!!this.state.selectedPinnedImage}
markedImageLoaded={this.state.markedImageLoaded}
handleMarkedImageLoaded={this.handleMarkedImageLoaded}
handleRequestClose={this.handlePinnedImageClose}
otherMarkedEntries={this.state.entries}
/>
}
<SimpleSnackbar showSnackbar={this.state.showSnackbar}
handleSnackbarClose={this.handleSnackbarClose}
snackbarMsg={this.state.snackbarMsg}/>
</div>}
</div>
);
}
}
}
}
export default withStyles(styles)(ViewJob);
You can define a regular component method and bind it in handler like this onSomething={this.handler.bind(this, index)} assuming you have some distinguishable thing in the index var
function should look like this
handler(index) {
...
}

Resources