Uncaught ReferenceError: React is not defined but no issue in component - javascript

wassup Folks,
I got the following error from my component called "SearchReactTableContainer". I looked inside but everything looks well to many. Any Ideas how to fix this? I looked into the network conditions and the needed data from my resulting query is provided without any issues to my frontend.
SearchReactTableContainer.js
import React, { Component, Fragment } from "react";
import { has, isNil } from "ramda";
import CockpitSearchInput from "#components/SearchInput";
import SearchReactTable from "#cockpitComponents/SearchReactTable";
import {
MY_CASES_FILTER_KEY,
UNASSIGNED_FILTER_KEY
} from "#modules/CaseManager/CaseManagerDashboard/constants";
import { Col, Row } from "antd";
// TODO: TO BE REMOVED
const pagesCount = 3855;
export default class SearchReactTableContainer extends Component {
constructor(props) {
super(props);
const { limit, searchValue } = this.props;
const orMyCases = localStorage.getItem(MY_CASES_FILTER_KEY);
const orUnassignedCases = localStorage.getItem(UNASSIGNED_FILTER_KEY);
this.state = {
loading: false,
searchValue: searchValue || "",
data: [],
pageSize: limit,
pagesCount,
orMyCases: this.parseNonNullValue(orMyCases),
orUnassignedCases: this.parseNonNullValue(orUnassignedCases)
};
this.triggerFetchMore = this.triggerFetchMore.bind(this);
}
parseNonNullValue(value) {
try {
return !isNil(value) ? JSON.parse(value) : true;
} catch (err) {
console.error(err);
return false;
}
}
async fetchMoreFromGraphql(fetchMore, variables, tableKey) {
return await fetchMore({
variables,
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult || !previousResult) return previousResult;
if (!(tableKey in previousResult)) {
return [];
}
return {
...previousResult,
...fetchMoreResult[tableKey]
};
}
});
}
async onFetchDataHandler(state) {
const { page, pageSize, sorted, filtered } = state;
const { tableKey, fetchMore } = this.props;
const { searchValue, orMyCases, orUnassignedCases } = this.state;
this.changeTableLoadingState();
const response = await this.fetchMoreFromGraphql(
fetchMore,
{
searchValue,
offset: page * pageSize,
limit: pageSize,
page,
pageSize,
sorted,
filtered,
filters: {
orMyCases,
orUnassignedCases
}
},
tableKey
);
let { data } = response;
const pagesCount =
searchValue.length === 0
? has("totalCount", data[tableKey])
? Math.ceil(data[tableKey].totalCount / pageSize)
: has("items", data[tableKey])
? Math.ceil(data[tableKey].items.length / pageSize)
: Math.ceil(data[tableKey].length / pageSize)
: this.state.pagesCount;
this.setState({
...this.state,
loading: false,
data: data[tableKey].items ? data[tableKey].items : data[tableKey],
pageIndex: page,
pageSize,
pagesCount
});
}
async triggerFetchMore(searchValue = "") {
const { tableKey, fetchMore, tableName } = this.props;
const { orMyCases, orUnassignedCases } = this.state;
this.changeTableLoadingState();
const variables = {
searchValue,
offset: 0,
limit: this.state.pageSize,
filters: {
orMyCases,
orUnassignedCases
}
};
const response = await this.fetchMoreFromGraphql(fetchMore, variables, tableKey);
const { data } = response;
try {
await this.setState(prevState => {
const { pageSize } = prevState;
const pagesCount =
searchValue.length === 0
? has("totalCount", data[tableKey])
? Math.ceil(data[tableKey].totalCount / pageSize)
: has("items", data[tableKey])
? Math.ceil(data[tableKey].items.length / pageSize)
: Math.ceil(data[tableKey].length / pageSize)
: this.state.pagesCount;
prevState = {
...prevState,
loading: false,
data: data[tableKey].items ? data[tableKey].items : data[tableKey],
searchValue,
pagesCount
};
return prevState;
});
localStorage.setItem(`${tableName}.searchValue`, searchValue);
} catch (err) {
console.log(err);
}
}
changeTableLoadingState() {
this.setState({ loading: !this.state.loading });
}
render() {
const {
columns,
dataFormatter,
search = true,
defaultSorted,
filterBlock
} = this.props;
const { data, pagesCount, pagesSize, loading, searchValue } = this.state;
let formattedData = data;
if (typeof dataFormatter === "function") {
formattedData = dataFormatter(data);
}
return (
<Fragment>
{search && (
<Row>
<Col span={6}>
<CockpitSearchInput
onChange={this.triggerFetchMore}
initialValue={searchValue}
/>
</Col>
</Row>
)}
{!isNil(filterBlock) && filterBlock(this)}
<SearchReactTable
{...this.props}
manual
data={formattedData}
columns={columns}
loading={loading}
// Request new data when things change
onFetchData={(state, instance) => this.onFetchDataHandler(state, instance)}
pages={pagesCount} // Display the total number of pages
pageSizeOptions={[5, 10, 20, 25, 50, 100]}
defaultPageSize={pagesSize}
className="table table-striped table-bordered table-hover"
defaultSorted={defaultSorted}
/>
</Fragment>
);
}
}
I'll add a screenshot showing me the actual issue in the developer tab.
Component that uses SearchReactTable

Related

Redux+Reactjs+NodeJS How to get a specific value from the database and display it to the screen by redux?

i am new, and am getting acquainted with i reactjs and nodejs, i am writing a website in which i want to write a function to get the current login information of the user through redux, and then after then assign the obtained user value to react-select, then from react-select, we select that user to perform the assignment of new data to the database from reactjs. However, I have not been able to get the logged in user information through redux. This is my code, anyone have any ideas? Thanks very much
here is my FE(Reactjs code):
here is DoctorManageSchedule.js:
import React, { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import './ManageSchedule.scss';
import Select from 'react-select';
import * as actions from "../../../store/actions";
import { CRUD_ACTIONS, LANGUAGES, dateFormat } from '../../../utils';
import DatePicker from '../../../components/Input/DatePicker';
import moment from 'moment';
import { toast } from "react-toastify";
import _ from 'lodash';
import { saveBulkScheduleDoctor, getScheduleDoctorById } from '../../../services/userService';
import DetailDoctor from './DetailDoctor';
class DoctorManageSchedule extends Component {
constructor(props) {
super(props);
this.state = {
arrDoctor: [],
selectedDoctor: {},
currentDate: '',
rangeTime: [],
minDate: moment().calendar(),
}
}
async componentDidMount() {
this.props.fetchDoctor(this.props.match.params.id);
this.props.fetchAllScheduleTimes();
}
async componentDidUpdate(prevProps, prevState, snapshot) {
if (prevProps.doctor !== this.props.doctor) {
let dataSelect = this.buildDataInputSelect(this.props.doctor)
this.setState({
arrDoctor: dataSelect
})
}
if (prevProps.allScheduleTime !== this.props.allScheduleTime) {
let data = this.props.allScheduleTime;
if (data && data.length > 0) {
data = data.map(item => ({ ...item, isSelected: false }))
}
this.setState({
rangeTime: data
})
}
}
buildDataInputSelect = (inputData) => {
let result = [];
let { language } = this.props;
if (inputData && inputData.length > 0) {
inputData.map((item, index) => {
let object = {};
let labelEn = `${item.lastName} ${item.firstName}`;
let labelVi = `${item.firstName} ${item.lastName}`;
object.label = language === LANGUAGES.VI ? labelVi : labelEn;
object.value = item.id;
result.push(object)
})
}
return result;
}
handleChangeSelect = async (selectedOption) => {
this.setState({ selectedDoctor: selectedOption });
}
handleOnChangeDatePicker = (date) => {
this.setState({
currentDate: date[0]
})
}
handleClickBtnTime = (time) => {
let { rangeTime } = this.state;
if (rangeTime && rangeTime.length > 0) {
rangeTime = rangeTime.map(item => {
if (item.id === time.id) item.isSelected = !item.isSelected;
return item;
})
this.setState({
rangeTime: rangeTime
})
}
}
handleSaveSchedule = async () => {
let { rangeTime, selectedDoctor, currentDate } = this.state;
let result = [];
if (!currentDate) {
toast.error("Invalid date!");
}
if (selectedDoctor && _.isEmpty(selectedDoctor)) {
toast.error("Invalid selected doctor! ");
console.log('check doctor: ', this.state)
return;
}
let formatedDate = new Date(currentDate).getTime();
if (rangeTime && rangeTime.length > 0) {
let selectedTime = rangeTime.filter(item => item.isSelected === true);
if (selectedTime && selectedTime.length > 0) {
selectedTime.map((schedule, index) => {
let object = {};
object.doctorId = selectedDoctor.value;
object.date = formatedDate;
object.timeType = schedule.keyMap;
result.push(object);
})
} else {
toast.error("Invalid selected time! ");
return;
}
}
let res = await saveBulkScheduleDoctor({
arrSchedule: result,
doctorId: selectedDoctor.value,
formatedDate: formatedDate
})
if (res && res.errCode === 0) {
toast.success("Save Infor succeed!");
} else {
toast.error("error saveBulkScheduleDoctor ");
console.log('error saveBulkScheduleDoctor >>> res: ', res)
}
console.log('bao phuc check result: ', result);
console.log('check res: saveBulkScheduleDoctor : ', res);
}
render() {
let { rangeTime, arrDoctor } = this.state;
console.log("check doctor:", arrDoctor)
let { language } = this.props;
let today = new Date(new Date().setDate(new Date().getDate()));
return (
<div className="manage-schedule-container">
<div className="m-s-title">
<FormattedMessage id="manage-schedule.title"></FormattedMessage>
</div>
<div className="container">
<div className="row">
<div className="col-6 form-group">
<label>
<FormattedMessage id="manage-schedule.choose-doctor" /> </label>
<Select
value={this.state.selectedDoctor}
onChange={this.handleChangeSelect}
options={this.state.listDoctors}
/>
</div>
<div className="col-6 form-group">
<label>
<FormattedMessage id="manage-schedule.choose-date" /> </label>
<DatePicker
value={this.state.currentDate}
className="form-control"
onChange={this.handleOnChangeDatePicker}
minDate={today}
/>
</div>
<div className="col-12 pick-hour-container">
{rangeTime && rangeTime.length > 0 &&
rangeTime.map((item, index) => {
return (
<button className={item.isSelected === true ?
"btn btn-schedule active" : "btn btn-schedule"}
key={index} onClick={() => this.handleClickBtnTime(item)}>
{language === LANGUAGES.VI ? item.valueVi : item.valueEn}
</button>
)
})}
</div>
<div className="col-12">
<button className="btn btn-primary btn-save-schedule"
onClick={() => this.handleSaveSchedule()}>
<FormattedMessage id="manage-schedule.save" />
</button>
</div>
</div>
</div>
</div>
);
}
}
const mapStateToProps = state => {
return {
language: state.app.language,
isLoggedIn: state.user.isLoggedIn,
doctor: state.admin.doctor,
allScheduleTime: state.admin.allScheduleTime,
};
};
const mapDispatchToProps = dispatch => {
return {
fetchDoctor: () => dispatch(actions.fetchDoctorStart()),
fetchAllScheduleTimes: () => dispatch(actions.fetchAllScheduleTimes())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(DoctorManageSchedule);
and here is my actionTypes.js:
const actionTypes = Object.freeze({
FETCH_DOCTOR_SUCCESS: 'FETCH_DOCTOR_SUCCESS',
FETCH_DOCTOR_FAILED: 'FETCH_DOCTOR_FAILED',
})
export default actionTypes;
and here is my adminActions.js:
export const fetchDoctorStart = id => () => {
return async (dispatch, getState) => {
try {
let res = await getScheduleDoctorById(id);
if (res && res.errCode === 0) {
dispatch({
type: actionTypes.FETCH_DOCTOR_SUCCESS,
dataDoctor: res.data
})
} else {
toast.error("Failed to fetch doctor");
dispatch(fetchDoctorFailed());
}
} catch (e) {
toast.error("Failed to fetch doctor");
dispatch(fetchDoctorFailed());
console.log("check fetch doctor failed: ", e);
}
}
};
export const fetchDoctorSuccess = (data) => ({
type: actionTypes.FETCH_DOCTOR_SUCCESS,
doctor: data
})
export const fetchDoctorFailed = () => ({
type: actionTypes.FETCH_DOCTOR_FAILED,
})
here is my adminReducer.js:
case actionTypes.FETCH_DOCTOR_SUCCESS:
state.doctor = action.dataDoctor;
return {
...state,
}
case actionTypes.FETCH_DOCTOR_FAILED:
state.doctor = [];
return {
...state,
}
here is my userService.js:
const getScheduleDoctorById = (inputId) => {
return axios.get(`/api/get-schedule-doctor-by-id?id=${inputId}`)
}
here is my BE(Nodejs code):
here is web.js:
router.get('/api/get-schedule-doctor-by-id', doctorController.getScheduleById);
here is doctorController.js:
let getScheduleById= async (req, res) => {
try {
let infor = await doctorService.getScheduleById(req.query.id);
return res.status(200).json(infor);
} catch (e) {
console.log(e);
return res.status(200).json({
errCode: -1,
errMessage: 'Error from the server'
})
}
}
here is doctorService.js:
let getScheduleById = (inputId) => {
return new Promise(async (resolve, reject) => {
try {
if (!inputId) {
resolve({
errCode: 1,
errMessage: 'Missing required parameter!'
})
} else {
let data = await db.User.findOne({
where: {
id: inputId
},
attributes: {
exclude: ['password']
},
include: [
{ model: db.Allcode, as: 'positionData', attributes: ['valueEn', 'valueVi'] },
{
model: db.Doctor_Infor,
attributes: {
exclude: ['id', 'doctorId']
}
},
],
raw: false,
nest: true
})
if (data && data.image) {
data.image = new Buffer(data.image, 'base64').toString('binary');
}
if (!data) data = {};
resolve({
errCode: 0,
data: data
})
}
} catch (e) {
reject(e);
}
})
}
when i run the app i only get the available time slots from the database, but no info about who is logged in, i checked the network tab, but it seems the api gets the user info via redux do not run. Or does anyone have a way to do it other than using redux, that the user (doctor) can set his own schedule and save it to the database? However, I don't know why, please comment, thanks a lot

How can the render be synced with the state on pagination click with react?

I created a suggestions search and its built to break up the fetch based on the current page. The state is console.loged correctly, but the render is one page click event behind. This is obviously not the behavior we want. It seems like the state is being updated fine. I have tried to refactor the code difference ways, and even tried this.forceUpdate()
Here is the code
SearchOrderBar.js
import React, { Component } from "react";
import {Input, Label, Table, Icon, Header, Menu} from 'semantic-ui-react';
import "./SearchOrderBar.css";
// import { resolve } from "dns";
// import PropTypes from 'prop-types';
import Pagination from '../Search/Pagination';
class SearchOrderBar extends Component {
constructor(props) {
super(props);
this.text = "";
this.state = {
suggestions: [],
addToQuery: false,
Query: [],
pagesNeeded: 0,
page: 1
};
let searchTerm = null;
const {pageLimit = null, keyTimer = null, } = props;
this.pageLimit = typeof pageLimit === 'number' ? pageLimit : 10;
this.handlePageClick = this.handlePageClick.bind(this);
this.fetchCallBack = this.fetchCallBack.bind(this);
// this.addToQuery = this.addToQuery.bind(this);
this.keyUpHandler = this.keyUpHandler.bind(this);
this.keyDownHandler = this.keyDownHandler.bind(this);
}
handlePageClick(page){
this.forceUpdate();
this.setState({
page: page
})
this.fetchCallBack();
}
//This fetch should be called in a dynamic switch case
fetchCallBack() {
let y = this.pageLimit;
let x = this.state.page > 1 ? (this.pageLimit*this.state.page) - this.pageLimit : 0;
// Return a promise
return new Promise((resolve, reject) => {
let searchTerm = this.searchTerm;
return fetch(`http://localhost:5000/api/searchorders/${searchTerm}/${x}/${y}`)
.then(res => {
if (!res.ok) {
throw res;
}
// Convert serialized response into json
return res.json()
}).then(data => {
//Use data
let searchTerm = data.map(data => {
let rData = {};
rData = data;
return rData;
})
this.item = searchTerm;
//console.log('here from callback')
this.setState({
suggestions: []
})
return searchTerm;
}).then( data => {
// console.log(this.totalRecords)sd
//console.log(data)
if (searchTerm.length === 0) {
this.setState({
suggestions: [],
rangeCount_URL: `http://localhost:5000/api/searchorderscount/${searchTerm}`
});
} else {
const suggestions = data.filter(function(v){
if(Object.values(v).includes(searchTerm.toLowerCase()) !== -1 || Object.values(v).includes(searchTerm.toUpperCase()) !== -1){
return v
}
})
console.log(suggestions)
this.text = searchTerm;
this.setState({ suggestions: suggestions.sort()});
}
})
})
}
pageCountCallBack(){
return new Promise((resolve, reject) => {
let searchTerm = this.searchTerm;
return fetch(`http://localhost:5000/api/searchorderscount/${searchTerm}/`)
.then(res => {
if (!res.ok) {
throw res;
}
// Convert serialized response into json
return res.json()
}).then(data => {
//Use data
let searchTerm = data.map(data => {
let rData = {};
rData = data;
return rData;
})
this.item = searchTerm;
// console.log('here from Page Count callback')
this.renderSuggestions();
resolve(searchTerm)
})
})
}
keyUpHandler = (e) => {
if(e.target.value.length >= 3){
this.keyTimer = setTimeout(this.countFetch(e), 1500);
} else {
this.setState(() => {
return {
suggestions : [],
pagesNeeded : 0
}
})
clearTimeout(this.keyTimer);
}
}
keyDownHandler = (e) => {
clearTimeout(this.keyTimer);
}
//Any time text is changed in the text field
countFetch = (e) => {
const value = e.target.value;
this.searchTerm = value;
this.pageCountCallBack().then(data => {
const totalRecords = data[0].rows;
this.setState(() => {
return {pagesNeeded : Math.ceil(totalRecords / this.pageLimit)}
})
//console.log("total" + totalRecords);
//console.log("page limit"+this.pageLimit);
//console.log("Needed" + this.state.pagesNeeded );
})
this.fetchCallBack();
}
renderSuggestions() {
//const { suggestions } = this.state;
const tableStyle = {
'tableLayout': 'fixed',
'overflowWrap': 'break-word'
}
return (
<Table style={tableStyle} celled>
{this.state.suggestions.length === 0 ?
(<Table.Body>
<Table.Cell colSpan="7">
<div className="ui fluid warning icon message">
<Icon name="exclamation triangle" size="huge" color="orange"/>
<div className="content">
<Header>No Records Found</Header>
<p>Try Seaching by one of the following:</p>
<ul>
<dt>Name</dt>
<dt>Order Number</dt>
<dt>Address (Shipping or Billing )</dt>
<dt>Phone Number</dt>
<dt>Email</dt>
</ul>
</div>
</div>
</Table.Cell>
</Table.Body>)
: (
<>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Order#</Table.HeaderCell>
<Table.HeaderCell>Billing Address</Table.HeaderCell>
<Table.HeaderCell>Shipping Address</Table.HeaderCell>
<Table.HeaderCell>Email</Table.HeaderCell>
<Table.HeaderCell>Phone Number</Table.HeaderCell>
<Table.HeaderCell>Sales Channel</Table.HeaderCell>
<Table.HeaderCell>Order Date</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{this.state.suggestions.map((item, index) => (
<Table.Row className="hoverRow">
<Table.Cell key={index} onClick={() => this.addToQuery(item)}>
{item.customerPO}
</Table.Cell>
<Table.Cell>
{item.billToAddress}
</Table.Cell>
<Table.Cell>{item.shipToAddress}</Table.Cell>
<Table.Cell>{item.email}</Table.Cell>
<Table.Cell>{item.phone}</Table.Cell>
<Table.Cell>{item.customerContact}</Table.Cell>
<Table.Cell>{item.dateCreated}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</>
)
}
<Pagination key={this.state.pagesNeeded} tableCols="7" pagesNeeded={this.state.pagesNeeded} btnLimit={5} pageClick={this.handlePageClick} currPage={this.state.page} pageLimit={this.pageLimit}/>
</Table>
);
}
handleIconClick(){
console.log('icon clicked ' + this.state.Query )
}
render() {
const {text} = this.state
//console.log(this.state)
return (
<>
<div className="App-Component">
<div className="App-Search">
<Input icon={{ name: 'search', circular: true, link: true, onClick: () => this.handleIconClick() }} placeholder="Search" value={text} type="text" onKeyUp={this.keyUpHandler} onKeyDown={this.keyDownHandler} className="App-Search"/>
{this.renderSuggestions()}
</div>
</div>
</>
);
}
}
export default SearchOrderBar;
Here is the pagination but I don't think this matters as much for the solution. It is relevant for the page button click.
import React, {Component} from 'react';
import {Input, Label, Table, Icon, Header, Menu} from 'semantic-ui-react'
/**
* Helper Method for creating a range of Numbers
* Range )( )
*/
const range = (from, to, step = 1) => {
let i = from;
const range = [];
while (i<=to) {
range.push(i);
i+=step;
}
}
export default class Pagination extends Component {
constructor(props){
super(props)
const { totalRecords = null, pageNeighbours = 0, rangeCount_URL = this.props.rangeCount_URL, pageArray = [] } = props;
this.pageArray = typeof pageArray === 'array' ? pageArray : [];
}
renderPagination = () => {
//console.log("hello from pagination");
let n = this.props.pagesNeeded;
let pArray = [];
let page = this.props.currPage;
//console.log(n)
if (page > 1){
pArray.push(<Menu.Item as='a' icon onClick={() => this.props.pageClick(page-1)}>
<Icon name='chevron left' />
</Menu.Item>)
}
for(let i = (page >1 ? page-1: page); pArray.length < (page > this.props.btnLimit ? this.props.btnLimit+1 : this.props.btnLimit); i++){
//console.log(i);
pArray.push(<Menu.Item index={i} className={i == page ? 'active' : ''} onClick={() => this.props.pageClick(i)} as='a'>{i}</Menu.Item>)
}
if (page < n){
pArray.push(<Menu.Item as='a' icon onClick={() => this.props.pageClick(page+1)}>
<Icon name='chevron right' />
</Menu.Item>)
}
this.pageArray = pArray;
return pArray;
}
render(){
const pageCount = (() => {
const totalRecords = this.totalRecords;
if(totalRecords > 0){
return (this.totalPages = Math.ceil(this.totalRecords / this.props.pageLimit))
}
})();
//console.log(this.pageArray);
return(
<Table.Footer>
{ this.props.pagesNeeded > 1 &&
<Table.Row>
<Table.HeaderCell colSpan={this.props.tableCols}>
<Menu floated='right' pagination>
{this.renderPagination()}
</Menu>
</Table.HeaderCell>
</Table.Row>
}
</Table.Footer>
)
}
}
setState is batched and invoked asynchronously, meaning when you call to this.setState({page}) then read this.state.page in fetchCallBack you probably get the "old" page and not the new page.
Either pass the page directly to fetchCallBack
this.fetchCallBack(page)
And read the page from it and not directly from the state
Or call it as the second argument of setState which is a callback that react will invoke right after the state has been updated.
this.setState({ page }, this.fetchCallBack);
At the point fetchCallBack is called, this.state.page is not updated yet because setState is called asynchronously, that's why it's using the old value. Try this:
handlePageClick(page) {
this.setState({ page }, this.fetchCallBack);
}
The callback syntax allows you to run the function in the next iteration.

Component did update returning always the same props and state

I found a lot of solutions about this problem but none of them work.
I have a view which renders dynamically components depending on the backend response
/**
* Module dependencies
*/
const React = require('react');
const Head = require('react-declarative-head');
const MY_COMPONENTS = {
text: require('../components/fields/Description'),
initiatives: require('../components/fields/Dropdown'),
vuln: require('../components/fields/Dropdown'),
severities: require('../components/fields/Dropdown'),
};
const request = restclient({
timeout: 5000,
baseURL: '/api',
});
const { DropdownItem } = Dropdown;
class CreateView extends React.Component {
constructor(props) {
super(props);
this.state = {
modal: false,
states: props.states,
error: props.error,
spinner: true,
state: props.state,
prevState: '',
components: [],
};
this.handleChange = this.handleChange.bind(this);
this.getRequiredFields = this.getRequiredFields.bind(this);
this.onChangeHandler = this.onChangeHandler.bind(this);
this.changeState = this.changeState.bind(this);
this.loadComponents = this.loadComponents.bind(this);
}
componentDidMount() {
this.loadComponents();
}
onChangeHandler(event, value) {
this.setState((prevState) => {
prevState.prevState = prevState.state;
prevState.state = value;
prevState.spinner = true;
return prevState;
}, () => {
this.getRequiredFields();
});
}
getRequiredFields() {
request.get('/transitions/fields', {
params: {
to: this.state.state,
from: this.state.prevState,
},
})
.then((response) => {
const pComponents = this.state.components.map(c => Object.assign({}, c));
pComponents.forEach((c) => {
c.field.required = 0;
c.field.show = false;
});
response.data.forEach((r) => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
ob.field.required = r.required;
ob.field.show = true;
}
});
this.setState({
components: pComponents,
fields: response.data,
spinner: false,
});
})
.catch(err => err);
}
loadComponents() {
this.setState((prevState) => {
prevState.components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
};
return {
field, component: MY_COMPONENTS[k],
};
});
return prevState;
});
}
handleChange(field, value) {
this.setState((prevState) => {
prevState[field] = value;
return prevState;
});
}
changeState(field, value) {
this.setState((prevState) => {
prevState[`${field}`] = value;
return prevState;
});
}
render() {
const Components = this.state.components;
return (
<Page name="CI" state={this.props} Components={Components}>
<Script src="vendor.js" />
<Card className="">
<div className="">
<div className="">
<Spinner
show={this.state.spinner}
/>
{Components.map((component, i) => {
const Comp = component.component;
return (<Comp
key={i}
value={this.state[component.field.name]}
field={component.field}
handleChange={this.handleChange}
modal={this.state.modal}
changeState={this.changeState}
/>);
})
}
</div>
</div>
</div>
</Card>
</Page>
);
}
}
module.exports = CreateView;
and the dropdown component
const React = require('react');
const request = restclient({
timeout: 5000,
baseURL: '/api',
});
const { DropdownItem } = Dropdown;
class DrpDwn extends React.Component {
constructor(props) {
super(props);
this.state = {
field: props.field,
values: [],
};
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('state', this.state.field);
console.log('prevState', prevState.field);
console.log('prevProps', prevProps.field);
console.log('props', this.props.field);
}
render() {
const { show } = this.props.field;
return (show && (
<div className="">
<Dropdown
className=""
onChange={(e, v) => this.props.handleChange(this.props.field.name, v)}
label={this.state.field.name.replace(/^./,
str => str.toUpperCase())}
name={this.state.field.name}
type="form"
value={this.props.value}
width={100}
position
>
{this.state.values.map(value => (<DropdownItem
key={value.id}
value={value.name}
primary={value.name.replace(/^./, str => str.toUpperCase())}
/>))
}
</Dropdown>
</div>
));
}
module.exports = DrpDwn;
The code actually works, it hide or show the components correctly but the thing is that i can't do anything inside componentdidupdate because the prevProps prevState and props are always the same.
I think the problem is that I'm mutating always the same object, but I could not find the way to do it.
What I have to do there is to fill the dropdown item.
Ps: The "real" code works, i adapt it in order to post it here.
React state is supposed to be immutable. Since you're mutating state, you break the ability to tell whether the state has changed. In particular, i think this is the main spot causing your problem:
this.setState((prevState) => {
prevState.components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
}; return {
field, component: MY_COMPONENTS[k],
};
});
return prevState;
});
You mutate the previous states to changes its components property. Instead, create a new state:
this.setState(prevState => {
const components = Object.keys(MY_COMPONENTS).map((k) => {
const field = {
name: k,
required: 0,
show: true,
};
return {
field, component: MY_COMPONENTS[k],
};
});
return { components }
}
You have an additional place where you're mutating state. I don't know if it's causing your particular problem, but it's worth mentioning anyway:
const pComponents = [].concat(this.state.components);
// const pComponents = [...this.state.components];
pComponents.forEach((c) => {
c.field.required = 0;
c.field.show = false;
});
response.data.forEach((r) => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
ob.field.required = r.required;
ob.field.show = true;
}
});
You do at make a copy of state.components, but this will only be a shallow copy. The array is a new array, but the objects inside the array are the old objects. So when you set ob.field.required, you are mutating the old state as well as the new.
If you want to change properties in the objects, you need to copy those objects at every level you're making a change. The spread syntax is usually the most succinct way to do this:
let pComponents = this.state.components.map(c => {
return {
...c,
field: {
...c.field,
required: 0,
show: false
}
}
});
response.data.forEach(r => {
const ob = pComponents.find(c => c.field.name === r.name);
if (ob) {
// Here it's ok to mutate, but only because i already did the copying in the code above
ob.field.required = r.required;
ob.field.show = true;
}
})

MobX Store Not Updating In React Native

I have implemented a MobX store in my React-Native app to keep track if a user is being followed or unfollowed. The follow/unfollow is registering, but the MobX store is not being updated. I am trying to update it directly with the this.follows.items[index] = { ...user, isFollowing: !user.isFollowing } but for some reason the store does not trigger an update.
Here is the View Component
#observer
class FollowsListView extends Component<Props> {
follows =
this.props.followType === 'followers'
? followsStore.getFollowersListLoader(this.props.userId)
: followsStore.getFollowingListLoader(this.props.userId);
componentDidMount = () => {
this.follows.lazyLoad();
};
render() {
return (
<>
<AppHeader
title={
this.props.followType === 'followers' ? 'FOLLOWERS' : 'FOLLOWING'
}
/>
<FlatList
contentContainerStyle={{ padding: 15 }}
data={this.follows.items}
keyExtractor={this.getId}
onEndReached={this.follows.loadMore}
onEndReachedThreshold={0.2}
onRefresh={this.follows.loadFromStart}
refreshing={this.follows.isLoading}
renderItem={this.renderFollows}
/>
</>
);
}
private getId = (user: { id: string }) => user.id;
renderUserActionButton(user: UserContainer) {
console.log(user);
return (
user.id !== _SessionManager.id && (
<TouchableOpacity
onPress={() => this.openActionMenu(user.following || user.owner)}
>
<Image source={Images.moreDots} />
</TouchableOpacity>
)
);
}
openActionMenu(user: User) {
const name = user.name || user.username;
const followOption = { name: 'follow', title: `Follow #${name}` };
const unfollowOption = { name: 'unfollow', title: `Unfollow #${name}` };
const options = {
customButtons: [user.isFollowing ? unfollowOption : followOption],
title: null,
takePhotoButtonTitle: null,
chooseFromLibraryButtonTitle: null,
};
ImagePicker.showImagePicker(options, ({ customButton }) => {
if (customButton === 'follow') {
this.props.changeIsFollowingUser(user.id, false);
}
if (customButton === 'unfollow') {
this.props.changeIsFollowingUser(user.id, true);
}
const index = this.follows.items.findIndex((user) => user.id);
this.follows.items[index] = { ...user, isFollowing: !user.isFollowing };
});
}
private renderFollows: ListRenderItem<UserContainer> = ({ item: user }) => {
const userId = user.following ? user.following.id : user.id;
return (
<UserRow
actionContent={this.renderUserActionButton(user)}
onPress={() => this.props.navigateTo('ProfilePublic', { userId })}
user={user.following || user.owner}
/>
);
};
}
const mapDispatchToProps = (dispatch: Function): MappedDispatch =>
bindActionCreators(
{
changeIsFollowingUser,
navigateTo,
},
dispatch
);
export default connect(
null,
mapDispatchToProps
)(FollowsListView);
Here is the Follows Store
import ListLoader from 'Network/ListLoader';
import { Follows } from 'Follows/Types';
import _API from 'Network/API';
class FollowsStore {
followers = new Map<string, Follows>();
followersList = new Map<string, ListLoader<Follows>>();
following = new Map<string, Follows>();
followingList = new Map<string, ListLoader<Follows>>();
getFollowersListLoader(userId: string) {
const list = this.followersList.get(userId);
if (list) return list;
const newList = new ListLoader<Follows>({
fetchData: async (params) => {
const url = `users/${userId}/followers`;
const response = await _API.get(url, { params });
return response.data;
},
onLoad: (data) => {
for (const user of data.items) {
this.followers.set(user.id, user);
}
},
});
this.followersList.set(userId, newList);
return newList;
}
getFollowingListLoader(userId: string) {
const list = this.followingList.get(userId);
if (list) return list;
const newList = new ListLoader<Follows>({
fetchData: async (params) => {
const url = `users/${userId}/following`;
const response = await _API.get(url, { params });
return response.data;
},
onLoad: (data) => {
for (const user of data.items) {
this.following.set(user.id, user);
}
},
});
this.followingList.set(userId, newList);
console.log(newList);
return newList;
}
}
const followsStore = new FollowsStore();
export default followsStore;
In MobX in order to change the state you would need to use an action. Set/decorate your openActionMenu as an action or extract the state changing code to another function which you decorate as action to be cleaner.

Bloated state in React

I have a React app with a bunch of components with a few similarities:
Most components load data from Firebase at construction
Most components have an input form that the user can interact with
Most components have a simple view
My issue is that the state becomes hard to manage fairly early on as I try to keep all state in the top level component. For instance, I have the component below that let's the user create a new product, add a few images and place a custom marker on one of the images.
My current setup for all components is that there is a currentEntry which represents the entry that the user is currently editing which I initialize with a blank state.
Is it best practice to keep all state in the top component like this or should I rethink my structure?
import React, { Component } from 'react';
import CreateEntryForm from "../../components/entries/createEntryForm";
import { withStyles } from 'material-ui/styles';
import ViewImageDialog from "../../components/entries/viewImageDialog";
import {FirebaseList} from "../../utils/firebase/firebaseList";
import {generateFilename, removeItem, snapshotToArray} from "../../utils/utils";
import {
Redirect
} from 'react-router-dom';
import AppBar from "../../components/appBar";
import Spinner from "../../components/shared/spinner";
import firebase from 'firebase';
const styles = theme => ({
root: {
margin: theme.spacing.unit*2,
}
});
const initialFormState = {
currentProduct: null,
selectedProducts: [],
selectedUploads: [],
selectedMarkedImage: null,
productQuantity: '',
locationDescription: '',
comments: '',
currentUpload: null,
username: 'username'
};
const initialFormErrorState = {
selectProductError: '',
};
class CreateEntry extends Component {
constructor() {
super();
this.state = {
products: [],
job: null,
currentEntry: {...initialFormState},
formErrors: initialFormErrorState,
uploadLoading: false,
markedImageLoaded: false,
attachmentDialogOpen: false,
openAttachment: null,
markerPosition: null,
availableAttachments: [],
entries: [],
redirect: false,
loading: true,
isEditing: false
};
this.firebase = new FirebaseList('entries');
this.handleSubmit = this.handleSubmit.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
this.setMarker = this.setMarker.bind(this);
this.handleAttachmentDialogOpen = this.handleAttachmentDialogOpen.bind(this);
this.saveMarkedImage = this.saveMarkedImage.bind(this);
this.handleMarkedImageLoaded = this.handleMarkedImageLoaded.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleProgress = this.handleProgress.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
this.handleUploadSuccess = this.handleUploadSuccess.bind(this);
}
componentDidMount() {
this.firebase.path = `entries/${this.props.match.params.id}`;
this.jobId = this.props.match.params.id;
this.entryId = this.props.match.params.entry || null;
this.firebase.db().ref(`jobs/${this.props.match.params.id}`).on('value', (snap) => {
const job = {
id: snap.key,
...snap.val()
};
this.setState({
job: job,
loading: false,
})
});
this.firebase.databaseSnapshot(`attachments/${this.jobId}`).then((snap) => {
const attachments = snapshotToArray(snap);
this.setState({availableAttachments: attachments})
});
this.firebase.databaseSnapshot(`entries/${this.jobId}`).then((snap) => {
const entries = snapshotToArray(snap);
const otherMarkedEntries = entries.filter(entry => entry.id !== this.entryId);
this.setState({otherMarkedEntries: otherMarkedEntries})
});
if (this.entryId) {
this.firebase.databaseSnapshot(`entries/${this.jobId}/${this.entryId}`).then((entry) => {
const updatedEntry = Object.assign({...initialFormState}, entry.val());
this.setState({
currentEntry: updatedEntry,
isEditing: !!this.entryId
})
});
}
}
validate() {
const errors = {...initialFormErrorState};
let isError = false;
if(this.state.currentEntry.selectedProducts.length === 0) {
errors.selectProductError = "You must select at least one product";
isError = true;
}
this.setState({formErrors: errors});
return isError
}
handleSubmit() {
const err = this.validate();
if(!err) {
if(this.state.job && this.state.currentEntry) {
if(!this.state.isEditing) {
const newEntry = {
...this.state.currentEntry,
'creationDate': Date.now()
};
let newEntryRef = this.firebase.db().ref(`entries/${this.jobId}`).push();
newEntryRef.set(newEntry);
if (this.state.currentEntry.selectedMarkedImage !== null) {
this.firebase.db().ref(`attachments/${this.jobId}/${newEntry.currentUpload.id}/markings/${newEntryRef.key}`)
.set(this.state.currentEntry.selectedMarkedImage)
}
this.setState({redirect: 'create'});
} else {
const updatedEntry = {
...this.state.currentEntry
};
const newLogEntry = {
'lastUpdated': Date.now(),
'updatedBy': 'username'
};
this.firebase.db().ref(`log/${this.jobId}/${this.entryId}`).push(newLogEntry);
this.firebase.update(this.entryId, updatedEntry)
.then(() => this.setState({redirect: 'edit'}));
}
}
}
};
handleInputChange = name => e => {
e.preventDefault();
const target = e.target;
const value = target.value;
if (name === 'currentUpload') {
this.handleAttachmentDialogOpen(this.state.job.selectedUploads);
}
this.setState({ currentEntry: { ...this.state.currentEntry, [name]: value } });
};
addSelectedChip = () => {
if (this.state.currentEntry.currentProduct) {
const updatedCurrentProduct = {
...this.state.currentEntry.currentProduct,
'productQuantity': this.state.currentEntry.productQuantity
};
const updatedSelectedProducts = [...this.state.currentEntry.selectedProducts, updatedCurrentProduct];
const updatedEntryStatus = {
...this.state.currentEntry,
selectedProducts: updatedSelectedProducts,
currentProduct: null,
productQuantity: ''
};
this.setState({currentEntry: updatedEntryStatus});
}
};
handleRequestDeleteChip = (data, group) => {
const itemToChange = new Map([['product', 'selectedProducts'], ['upload', 'selectedUploads']]);
const selected = itemToChange.get(group);
const updatedSelectedItems = removeItem(this.state.currentEntry[selected], data.id);
const updatedEntryStatus = {
...this.state.currentEntry,
[selected]: updatedSelectedItems
};
this.setState({currentEntry: updatedEntryStatus});
};
handleAttachmentDialogOpen = (attachment) => {
this.setState({
attachmentDialogOpen: true,
openAttachment: attachment
});
};
handleAttachmentDialogClose =() => {
this.setState({attachmentDialogOpen: false})
};
saveMarkedImage() {
const markedImage = {
'attachment': this.state.openAttachment[0],
'position': this.state.markerPosition
};
const updatedCurrentEntry = {
...this.state.currentEntry,
'selectedMarkedImage': markedImage
};
this.setState({
currentEntry: updatedCurrentEntry
});
this.handleAttachmentDialogClose()
}
setMarker(e) {
const dim = e.target.getBoundingClientRect();
const position = {
'pageX': e.pageX - dim.left -25,
'pageY': e.pageY - dim.top - 50
};
this.setState({markerPosition: position});
}
handleMarkedImageLoaded() {
this.setState({markedImageLoaded: true})
}
filterProducts(selected, available) {
if(this.state.job) {
const selectedProductNames = [];
selected.forEach(product => selectedProductNames.push(product.name));
return available.filter(product => !selectedProductNames.includes(product.name))
}
}
handleUploadStart = () => this.setState({uploadLoading: true, progress: 0});
handleProgress = (progress) => this.setState({progress});
handleUploadError = (error) => {
this.setState({uploadLoading: false});
console.error(error);
};
handleUploadSuccess = (filename) => {
firebase.storage().ref('images').child(filename).getDownloadURL().then(url => {
const getNameString = (f) => f.substring(0,f.lastIndexOf("_"))+f.substring(f.lastIndexOf("."));
const uploadItem = {"name": getNameString(filename), "url": url, "id": this.generateRandom()};
const updatedSelectedUploads = [...this.state.currentEntry.selectedUploads, uploadItem];
const updatedEntryStatus = {
...this.state.currentEntry,
selectedUploads: updatedSelectedUploads
};
this.setState({
uploadLoading: false,
currentEntry: updatedEntryStatus
});
});
};
generateRandom() {
return parseInt(Math.random());
}
render() {
const {classes} = this.props;
const filteredProducts = this.filterProducts(this.state.currentEntry.selectedProducts, this.state.job && this.state.job.selectedProducts);
const title = this.state.isEditing ? "Edit entry for" : "Add entry for";
const redirectRoute = this.state.redirect
? `/entries/${this.props.match.params.id}/${this.state.redirect}`
: `/entries/${this.props.match.params.id}`;
return (
<section>
<AppBar title={`${title} ${this.state.job && this.state.job.jobId}`} route={`/entries/${this.props.match.params.id}`}/>
{this.state.loading
? <Spinner />
: <div className={classes.root}>
<ViewImageDialog open={this.state.attachmentDialogOpen}
handleRequestClose={this.handleAttachmentDialogClose}
attachment={this.state.currentEntry.currentUpload}
setMarker={this.setMarker}
markerPosition={this.state.markerPosition || this.state.selectedMarkedImage && this.state.selectedMarkedImage.position}
saveMarkedImage={this.saveMarkedImage}
markedImageLoaded={this.state.markedImageLoaded}
handleMarkedImageLoaded={this.handleMarkedImageLoaded}
otherMarkedEntries={this.state.otherMarkedEntries}
/>
<CreateEntryForm handleInputChange={this.handleInputChange}
handleSubmit={this.handleSubmit}
availableProducts={filteredProducts}
addSelectedChip={this.addSelectedChip}
handleRequestDeleteChip={this.handleRequestDeleteChip}
job={this.state.job}
availableAttachments={this.state.availableAttachments}
uploadLoading={this.state.uploadLoading}
handleAttachmentDialogOpen={this.handleAttachmentDialogOpen}
markedImageLoaded={this.state.markedImageLoaded}
handleMarkedImageLoaded={this.handleMarkedImageLoaded}
isEditing={this.state.isEditing}
handleProgress={this.handleProgress}
handleUploadError={this.handleUploadError}
handleUploadSuccess={this.handleUploadSuccess}
firebaseStorage={firebase.storage().ref('images')}
filename={file => generateFilename(file)}
otherMarkedEntries={this.state.otherMarkedEntries}
{...this.state.currentEntry}
{...this.state.formErrors}
/>
{this.state.redirect && <Redirect to={redirectRoute} push />}
</div>}
</section>
);
}
}
export default withStyles(styles)(CreateEntry);
A centralised global state is a good pattern for state that needs to be global to the whole application. For me, https://redux.js.org/ is the best state engine for react applications.
When I build react/redux applications, I tend to start storing state at the lowest component level I can, and then move it up the component tree and finally into global redux state as and when it is required.
For example, a piece of state that stores whether a div is being hovered over could be stored at component level because it doesn't affect other components, but a piece of state that stores whether a modal is open might need to be in global redux state, because other parts of the application would need to know this.
I would really recommend trying out redux, or at least reading the docs.

Categories

Resources