How to add page number to the URL - reactjs

Could someone please tell me how can I add page number to my url. The component is as follows:
/** NPM Packages */
import React, { Component } from "react";
import { connect } from "react-redux";
import { Spinner, Pagination } from "react-bootstrap";
//import styles from "./App.module.css";
/** Custom Packages */
import List from "../List";
//import fetchCategories from "../../../actions/configuration/category/fetchCategories";
import deleteCategory from "../../../actions/configuration/category/deleteCategory";
import API from "../../../../app/pages/utils/api";
class Category extends Component {
constructor(props) {
super(props);
this.state = {
mesg: "",
mesgType: "",
isLoading: true,
total: null,
per_page: null,
current_page: 1,
pdata: []
};
this.fetchCategoriesAPI = this.fetchCategoriesAPI.bind(this);
}
fetchCategoriesAPI = async pno => {
await API.get("categories?offset=" + (pno.index+1))
.then(res => this.setState({ pdata: res.data }))
.then(() => this.props.passToRedux(this.state.pdata))
.catch(err => console.log(err));
};
componentDidMount = async () => {
const { state } = this.props.location;
if (state && state.mesg) {
this.setState({
mesg: this.props.location.state.mesg,
mesgType: this.props.location.state.mesgType
});
const stateCopy = { ...state };
delete stateCopy.mesg;
this.props.history.replace({ state: stateCopy });
}
this.closeMesg();
await this.fetchCategoriesAPI(1);
this.setState({ isLoading: false });
};
onDelete = async id => {
this.props.removeCategory(id);
await deleteCategory(id).then(data =>
this.setState({ mesg: data.msg, mesgType: "success" })
);
this.closeMesg();
};
closeMesg = () =>
setTimeout(
function() {
this.setState({ mesg: "", mesgType: "" });
}.bind(this),
10000
);
/** Rendering the Template */
render() {
let activePage = this.state.pdata.currPage;
let items = [];
let totalPages = Math.ceil(this.state.pdata.totalCount / 10);
for (let number = 1; number <= totalPages; number++) {
items.push(
<Pagination.Item key={number} active={number == activePage}>
{number}
</Pagination.Item>
);
}
const paginationBasic = (
<div>
<Pagination>
{items.map((item,index)=>{
return <p key={index} onClick={() => this.fetchCategoriesAPI({index})}>{item}</p>
})}
</Pagination>
<br />
</div>
);
const { mesg, mesgType, isLoading } = this.state;
return (
<>
{mesg ? (
<div
className={"alert alert-" + mesgType + " text-white mb-3"}
role="alert"
>
{mesg}
</div>
) : (
""
)}
{isLoading ? (
<div className="container-fluid">
<h4
className="panel-body"
style={{ "text-align": "center", margin: "auto" }}
>
Loading
<Spinner animation="border" role="status" />
</h4>
</div>
) : (
<div>
<List
listData={this.props.categories}
listName="category"
_handleDelete={this.onDelete.bind(this)}
/>
{paginationBasic}
</div>
)}
</>
);
}
}
const matchStatestoProps = state => {
return { categories: state.categories };
};
const dispatchStatestoProps = dispatch => {
return {
passToRedux: pload =>
dispatch({ type: "FETCH_CATEGORIES", payload: pload }),
removeCategory: id => dispatch({ type: "DELETE_CATEGORY", payload: id })
};
};
export default connect(matchStatestoProps, dispatchStatestoProps)(Category);
the route is as follows:
<Route exact path="/categories/:page?" component={Category} />
So basically I want the page number to be displayed in the URL. Also if I change the page number, the data should load the corresponding page. Please help me
Could someone please help me out?

In a class component:
Your router will pass match in as a prop. When your component mounts, get this.props.match.params.page and load the data accordingly:
class MyComponent extends React.Component {
componentDidMount () {
// get the 'page' param out of the router props.
// default to 0 if not specified.
const { page = 0 } = this.props.match.params;
// it comes in as a string, parse to int
const p = parseInt(page, 10);
// do whatever you need to do (load data, etc.)
}
}
In a function component:
In a function component, you can get the page param via react-router's useParams hook:
import { useParams } from 'react-router-dom';
function MyComponent () {
const { page } = useParams(); // get the 'page' router param
const p = parseInt(page, 10); // comes in as a string, convert to int
// do whatever you need to do with it
}
If you need prev/next navigation you can deduce those page numbers from the current page.
I made this quick example that demonstrates how to access and use the route's url parameters via react router's useParams hook and how to do it via the match prop with a class component.

You can get page number from props like this:
const matchStatestoProps = (state, ownProps) => {
return { id: ownProps.match.params.id; categories: state.categories };
};
In your routes:
<Route path="/page/:id" component={Page} />

Related

Custom react hook triggers api call multiple times

I cannot figure out how to handle my function components calling my api repeatedly. I have two components which retrieve data, one of them calls the api twice. Once before the second component once after.
I am using a custom react hook and axios get method to retrieve the data. My two components are are nested. The first component when loads and fetches data. Inside this component is a child component which when renders it fetches data right before passing the first set of data as props to another child component. When it completes loading it reloads the first child component which again calls the api for data. I understand the function components reload on state change. I would be happy for it to not call the api a second time. Is there a way to check if it already has data and bypass the api call?
Custom hook to retrieve data
import React, { useState, useEffect, useReducer } from "react";
import axios from "axios";
const dataFetchReducer = (state, action) => {
switch (action.type) {
case "FETCH_INIT":
return { ...state, isLoading: true, hasErrored: false };
case "FETCH_SUCCESS":
return {
...state,
isLoading: false,
hasErrored: false,
errorMessage: "",
data: action.payload
};
case "FETCH_FAILURE":
return {
...state,
isLoading: false,
hasErrored: true,
errorMessage: "Data Retrieve Failure"
};
case "REPLACE_DATA":
// The record passed (state.data) must have the attribute "id"
const newData = state.data.map(rec => {
return rec.id === action.replacerecord.id ? action.replacerecord : rec;
});
return {
...state,
isLoading: false,
hasErrored: false,
errorMessage: "",
data: newData
};
default:
throw new Error();
}
};
const useAxiosFetch = (initialUrl, initialData) => {
const [url] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
hasErrored: false,
errorMessage: "",
data: initialData
});
useEffect(() => {
let didCancel = false;
const fetchData = async () => {
dispatch({ type: "FETCH_INIT" });
try {
let result = await axios.get(url);
if (!didCancel) {
dispatch({ type: "FETCH_SUCCESS", payload: result.data });
}
} catch (err) {
if (!didCancel) {
dispatch({ type: "FETCH_FAILURE" });
}
}
};
fetchData();
return () => {
didCancel = true;
};
}, [url]);
const updateDataRecord = record => {
dispatch({
type: "REPLACE_DATA",
replacerecord: record
});
};
return { ...state, updateDataRecord };
};
export default useAxiosFetch;
Main component which renders the "CompaniesDropdown" twice inside
CompaniesDropdown is one of three dropdowns within the ListFilterContainer component but the only one which calls the api more than once. The other two dropdowns load by selection of the CompaniesDropdown.
import React, { useMemo, useEffect, useContext } from "react";
import InvoiceList from "../src/Components/Lists/InvoiceList";
import useAxiosFetch from "../src/useAxiosFetch";
import { ConfigContext } from "./_app";
import ListFilterContainer from "../src/Components/Filters/InvoiceFilters";
// import "../css/ListView.css";
const Invoices = props => {
const context = useContext(ConfigContext);
useEffect(() => {
document.title = "Captive Billing :: Invoices";
});
const {
data,
isLoading,
hasErrored,
errorMessage,
updateDataRecord
} = useAxiosFetch("https://localhost:44394/Invoice/GetInvoices/false", []);
const newInvoicesList = useMemo(
() => data
// .filter(
// ({ sat, sun }) => (speakingSaturday && sat) || (speakingSunday && sun)
// )
// .sort(function(a, b) {
// if (a.firstName < b.firstName) {
// return -1;
// }
// if (a.firstName > b.firstName) {
// return 1;
// }
// return 0;
// }),
// [speakingSaturday, speakingSunday, data]
);
const invoices = isLoading ? [] : newInvoicesList;
if (hasErrored)
return (
<div>
{errorMessage} "Make sure you have launched "npm run json-server"
</div>
);
if (isLoading) return <div>Loading...</div>;
const dataProps = {
data: invoices,
titlefield: "invoiceNumber",
titleHeader: "Invoice Number:",
childPathRoot: "invoiceDetail",
childIdField: "invoiceId",
childDataCollection: "invoiceData"
};
var divStyle = {
height: context.windowHeight - 100 + "px"
};
return (
<main>
<ListFilterContainer />
<section style={divStyle} id="invoices" className="card-container">
<InvoiceList data={dataProps} />
</section>
</main>
);
};
Invoices.getInitialProps = async ({ req }) => {
const isServer = !!req;
return { isServer };
};
export default Invoices;
Actual result is described above. My main concern is to not have the api calls more than once.
Here is some additional code to help. It is the filter control mentioned above. It, as you will notice really just contains dropdowns and a text box. The first dropdown is the one that calls the api twice. The second two are not visible until that one is selected.
import React, { useState, useMemo } from "react";
import CompaniesDropdown from "../Dropdowns/CompaniesDropdown";
import LocationsDropdown from "../Dropdowns/LocationsDropdown";
import AccountsDropdown from "../Dropdowns/AccountsDropdown";
import Search from "./SearchFilter/SearchFilter";
const InvoiceFilters = props => {
const [company, setCompany] = useState("");
const [location, setLocation] = useState(undefined);
const [account, setAccount] = useState(undefined);
const handleClientChange = clientValue => {
setCompany(clientValue);
};
const handleLocationsChange = locationValue => {
setLocation(locationValue);
};
const handleAccountsChange = AccountValue => {
setAccount(AccountValue);
};
return (
<section className="filter-container mb-3">
<div className="form-row">
<div className="col-auto">
<CompaniesDropdown change={e => handleClientChange(e)} />
</div>
<div className="col-auto">
<LocationsDropdown
selectedCompany={company}
change={e => handleLocationsChange(e)}
/>
</div>
<div className="col-auto">
<AccountsDropdown
selectedCompany={company}
change={e => handleAccountsChange(e)}
/>
</div>
<div className="col-auto">
<Search />
</div>
</div>
</section>
);
};
InvoiceFilters.getInitialProps = async ({ req }) => {
const isServer = !!req;
return { isServer };
};
export default InvoiceFilters;
Also the datalist
import React from "react";
import Link from "next/link";
import InvoiceListRecord from "./InvoiceListRecord";
const InvoiceList = props => {
let dataCollection = props.data.data;
return dataCollection.length == 0 ? "" : dataCollection.map((item, index) => {
return (
<section key={"item-" + index} className="card text-left mb-3">
<header className="card-header">
<span className="pr-1">{props.data.titleHeader}</span>
<Link
href={
"/" +
props.data.childPathRoot +
"?invoiceId=" +
item[props.data.childIdField]
}
as={
"/" +
props.data.childPathRoot +
"/" +
item[props.data.childIdField]
}
>
<a>{item[props.data.titlefield]}</a>
</Link>{" "}
</header>
<div className="card-body">
<div className="row">
<InvoiceListRecord
data={item}
childDataCollection={props.data.childDataCollection}
/>
</div>
</div>
</section>
);
});
};
InvoiceList.getInitialProps = async ({ req }) => {
console.log("Get Intitial Props works: Invoices Page!");
const isServer = !!req;
return { isServer };
};
export default InvoiceList;
and the list items component.
import React from "react";
const InvoiceListRecord = props => {
var invoiceData = JSON.parse(props.data[props.childDataCollection]);
return invoiceData.map((invKey, index) => {
return (
<div className="col-3 mb-1" key={"item-data-" + index}>
<strong>{invKey.MappedFieldName}</strong>
<br />
{invKey.Value}
</div>
);
});
};
export default InvoiceListRecord;
The API is not called more than once if the url is the same. It just gets the value from data variable. The api call is not made again, unless the url changes.
I created an example from your code, changing all the unknown components to div. I added a console.log in the useEffect of the useAxiosFetch hook. And to re-render the component, I added a button to increment the count.
You'll see that the console.log from the hook is printed only once, even though the component re-renders on every button click. The value just comes from the data variable from the hook and the api call is not made again and again.

Passing props to Parent component

I am really novice to React and I am stuck with this one.
I want to pass data from NewAction component to its parent NewActionSet.
I dont know what i am missing.
I am developing an on-boarding platform with a lot a components and I aim to send all the data entered into all the components to a server.
React parent Component:
import React from 'react'
import './NewActionSet.css'
import axios from 'axios'
import { Container, Segment, Header, Input } from 'semantic-ui-react'
import NewAction from './NewAction'
import 'bootstrap/dist/css/bootstrap.min.css'
class NewActionSet extends React.Component {
constructor (props) {
super(props)
this.state = {
actions: [],
actionType: '',
actionValue: '',
creationStatus: undefined
}
}
handleActions = value => {
this.setState({
actionsList: value
})
console.log(this.state.actionsList)
}
handleSubmit = event => {
event.preventDefault()
console.log(this.state)
axios
.post(
'/assistant/actions/',
{ ...this.state.values },
{ headers: {
xsrfHeaderName: 'X-CSRFToken',
xsrfCookieName: 'csrftoken'
},
withCredentials: true
}
)
.then(response => {
console.log(response)
this.setState({
creationStatus: true
})
})
.catch(error => {
console.log(error)
this.setState({
creationStatus: false
})
})
}
addNewAction = () => {
let { actions } = this.state
this.setState({
actions: [...actions, <NewAction onNewAction={this.handleActionstoParent} />]
})
}
handleActionstoParent = (action2Value, selectedAction) => {
this.setState({
actionType : selectedAction,
actionValue : action2Value
})
// console.log(this.state.actionType, this.state.actiondValue)
}
renderActions () {
return this.state.actions.map((action, index) => {
return (
<NewAction
key={index}
type={this.props.actionType}
content={action.content}
onNewAction={this.handleActionstoParent}
/>
)
})
}
render () {
let index = 0
return (
<Container>
<Header> Action sets </Header>
<Header color='grey' as='h3'>
SET #{index + 1}
</Header>
{this.renderActions()}
<button onClick={() => this.addNewAction()}> New Action </button>
</Container>
)
}
}
export default NewActionSet
React child component
import React from 'react'
import './NewActionSet.css'
import { Header, Dropdown } from 'semantic-ui-react'
import NewSpeechText from './NewSpeechText'
import NewAddPageURL from './NewAddPageURL'
import 'bootstrap/dist/css/bootstrap.min.css'
class NewAction extends React.Component {
constructor (props) {
super(props)
this.state = {
availableActions: [
{ key: 1, text: 'Navigate to page', value: 'Navigate to page' },
{ key: 2, text: 'Play speech', value: 'Play speech' }
],
selectedAction: '',
actionValue: '',
currentElement: ''
}
}
handleActionURL = (value) => {
this.setState({
actionValue: value
})
console.log(this.state.selectedAction, this.state.actionValue)
}
handleActionSpeech = (value) => {
this.setState({
actionValue: value
})
console.log(this.state.selectedAction, this.state.actionValue)
}
// Props to pass data to parent component --> NewActionSet.js
handleActionstoParent = (selected) => {
var action2Value = this.state.actionValue;
console.log(action2Value)
var action2Type = this.state.actionType
this.props.onNewAction(action2Value, action2Type)
console.log(action2Type)
// console.log(this.state.actionValue, this.state.selectedAction)
}
handleChange = (e, { value }) => {
let element
this.setState({
selectedAction: value
})
if (value === 'Navigate to page') {
element = <NewAddPageURL onNewAddPageURL={this.handleActionURL} onChange={this.handleActionstoParent()} />
} else if (value === 'Play speech') {
element = <NewSpeechText onNewSpeechText={this.handleActionSpeech} onChange={this.handleActionstoParent()} />
}
this.setState({
currentElement: element
})
}
render () {
const { value } = this.state
let index = 0
return (
<div className='action'>
<div className='container'>
<Header color='grey' as='h4'>
ACTION #{index + 1}
</Header>
<div className='row'>
<div className='col-md-4'>
<Dropdown
onChange={this.handleChange}
options={this.state.availableActions}
placeholder='Choose an action'
selection
value={value}
/>
</div>
<div className='col-md-4' />
<div className='col-md-4' />
</div>
<div style={{ marginBottom: '20px' }} />
{this.state.currentElement}
</div>
</div>
)
}
}
export default NewAction
Can you please assist?
Thanks a lot
The handleActionstoParent function in NewAction component is the problem.
When you send data from child to parent, actually the data is not updated data.
// Props to pass data to parent component --> NewActionSet.js
handleActionstoParent = (e) => {
this.setState({ [e.target.name]: e.target.value }, () => {
var action2Value = this.state.actionValue;
var action2Type = this.state.actionType;
this.props.onNewAction(action2Value, action2Type);
});
}
You could pass a function to NewAction, in example below we pass handleDataFlow function to our child component and then use it in our child component to pass data higher:
import React from 'react'
import './NewActionSet.css'
import { Header, Dropdown } from 'semantic-ui-react'
import NewSpeechText from './NewSpeechText'
import NewAddPageURL from './NewAddPageURL'
import 'bootstrap/dist/css/bootstrap.min.css'
class NewAction extends React.Component {
constructor (props) {
super(props)
this.state = {
availableActions: [
{ key: 1, text: 'Navigate to page', value: 'Navigate to page' },
{ key: 2, text: 'Play speech', value: 'Play speech' }
],
selectedAction: '',
actionValue: '',
currentElement: ''
}
}
handleActionURL = (value) => {
this.setState({
actionValue: value
})
console.log(this.state.selectedAction, this.state.actionValue)
}
handleActionSpeech = (value) => {
this.setState({
actionValue: value
})
console.log(this.state.selectedAction, this.state.actionValue)
}
// Props to pass data to parent component --> NewActionSet.js
handleActionstoParent = (selected) => {
var action2Value = this.state.actionValue;
console.log(action2Value)
var action2Type = this.state.actionType
this.props.onNewAction(action2Value, action2Type)
console.log(action2Type)
// console.log(this.state.actionValue, this.state.selectedAction)
}
handleChange = (e, { value }) => {
let element
this.setState({
selectedAction: value
})
this.props.handleDataFlow(value)
if (value === 'Navigate to page') {
element = <NewAddPageURL onNewAddPageURL={this.handleActionURL} onChange={this.handleActionstoParent()} />
} else if (value === 'Play speech') {
element = <NewSpeechText onNewSpeechText={this.handleActionSpeech} onChange={this.handleActionstoParent()} />
}
this.setState({
currentElement: element
})
}
render () {
const { value } = this.state
let index = 0
return (
<div className='action'>
<div className='container'>
<Header color='grey' as='h4'>
ACTION #{index + 1}
</Header>
<div className='row'>
<div className='col-md-4'>
<Dropdown
onChange={this.handleChange}
options={this.state.availableActions}
placeholder='Choose an action'
selection
value={value}
/>
</div>
<div className='col-md-4' />
<div className='col-md-4' />
</div>
<div style={{ marginBottom: '20px' }} />
{this.state.currentElement}
</div>
</div>
)
}
}
export default NewAction
Data flow in React is unidirectional. Data has one, and only one, way to be transferred: from parent to child.
To update parent state from child you have to send action (in props).
<NewAction updateParentState={this.doSmth} />
...
const doSmth = params => { this.setState({ ... })
and in NewAction you can call it in specific case
let parentUpdateState = ....
this.props.updateParentState(parentUpdateState);

Firebase/React/Redux Component has weird updating behavior, state should be ok

I am having a chat web app which is connected to firebase.
When I refresh the page the lastMessage is loaded (as the gif shows), however, for some reason, if the component is otherwise mounted the lastMessage sometimes flickers and disappears afterwards like it is overridden. When I hover over it, and hence update the component, the lastMessage is there.
This is a weird behavior and I spent now days trying different things.
I would be very grateful if someone could take a look as I am really stuck here.
The db setup is that on firestore the chat collection has a sub-collection messages.
App.js
// render property doesn't re-mount the MainContainer on navigation
const MainRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props => (
<MainContainer>
<Component {...props} />
</MainContainer>
)}
/>
);
render() {
return (
...
<MainRoute
path="/chats/one_to_one"
exact
component={OneToOneChatContainer}
/>
// on refresh the firebase user info is retrieved again
class MainContainer extends Component {
componentDidMount() {
const { user, getUserInfo, firebaseAuthRefresh } = this.props;
const { isAuthenticated } = user;
if (isAuthenticated) {
getUserInfo(user.id);
firebaseAuthRefresh();
} else {
history.push("/sign_in");
}
}
render() {
return (
<div>
<Navigation {...this.props} />
<Main {...this.props} />
</div>
);
}
}
Action
// if I set a timeout around fetchResidentsForChat this delay will make the lastMessage appear...so I must have screwed up the state / updating somewhere.
const firebaseAuthRefresh = () => dispatch => {
firebaseApp.auth().onAuthStateChanged(user => {
if (user) {
localStorage.setItem("firebaseUid", user.uid);
dispatch(setFirebaseAuthUser({uid: user.uid, email: user.email}))
dispatch(fetchAllFirebaseData(user.projectId));
}
});
};
export const fetchAllFirebaseData = projectId => dispatch => {
const userId = localStorage.getItem("firebaseId");
if (userId) {
dispatch(fetchOneToOneChat(userId));
}
if (projectId) {
// setTimeout(() => {
dispatch(fetchResidentsForChat(projectId));
// }, 100);
...
export const fetchOneToOneChat = userId => dispatch => {
dispatch(requestOneToOneChat());
database
.collection("chat")
.where("userId", "==", userId)
.orderBy("updated_at", "desc")
.onSnapshot(querySnapshot => {
let oneToOne = [];
querySnapshot.forEach(doc => {
let messages = [];
doc.ref
.collection("messages")
.orderBy("created_at")
.onSnapshot(snapshot => {
snapshot.forEach(message => {
messages.push({ id: message.id, ...message.data() });
});
});
oneToOne.push(Object.assign({}, doc.data(), { messages: messages }));
});
dispatch(fetchOneToOneSuccess(oneToOne));
});
};
Reducer
const initialState = {
residents: [],
oneToOne: []
};
function firebaseChat(state = initialState, action) {
switch (action.type) {
case FETCH_RESIDENT_SUCCESS:
return {
...state,
residents: action.payload,
isLoading: false
};
case FETCH_ONE_TO_ONE_CHAT_SUCCESS:
return {
...state,
oneToOne: action.payload,
isLoading: false
};
...
Main.js
// ...
render() {
return (...
<div>{React.cloneElement(children, this.props)}</div>
)
}
OneToOne Chat Container
// without firebaseAuthRefresh I don't get any chat displayed. Actually I thought having it inside MainContainer would be sufficient and subscribe here only to the chat data with fetchOneToOneChat.
// Maybe someone has a better idea or point me in another direction.
class OneToOneChatContainer extends Component {
componentDidMount() {
const { firebaseAuthRefresh, firebaseData, fetchOneToOneChat } = this.props;
const { user } = firebaseData;
firebaseAuthRefresh();
fetchOneToOneChat(user.id || localStorage.getItem("firebaseId"));
}
render() {
return (
<OneToOneChat {...this.props} />
);
}
}
export default class OneToOneChat extends Component {
render() {
<MessageNavigation
firebaseChat={firebaseChat}
firebaseData={firebaseData}
residents={firebaseChat.residents}
onClick={this.selectUser}
selectedUserId={selectedUser && selectedUser.residentId}
/>
}
}
export default class MessageNavigation extends Component {
render() {
const {
onClick,
selectedUserId,
firebaseChat,
firebaseData
} = this.props;
<RenderResidentsChatNavigation
searchChat={this.searchChat}
residents={residents}
onClick={onClick}
firebaseData={firebaseData}
firebaseChat={firebaseChat}
selectedUserId={selectedUserId}
/>
}
}
const RenderResidentsChatNavigation = ({
residents,
searchChat,
selectedUserId,
onClick,
firebaseData,
firebaseChat
}) => (
<div>
{firebaseChat.oneToOne.map(chat => {
const user = residents.find(
resident => chat.residentId === resident.residentId
);
const selected = selectedUserId == chat.residentId;
if (!!user) {
return (
<MessageNavigationItem
id={chat.residentId}
key={chat.residentId}
chat={chat}
onClick={onClick}
selected={selected}
user={user}
firebaseData={firebaseData}
/>
);
}
})}
{residents.map(user => {
const selected = selectedUserId == user.residentId;
const chat = firebaseChat.oneToOne.find(
chat => chat.residentId === user.residentId
);
if (_isEmpty(chat)) {
return (
<MessageNavigationItem
id={user.residentId}
key={user.residentId}
chat={chat}
onClick={onClick}
selected={selected}
user={user}
firebaseData={firebaseData}
/>
);
}
})}
</div>
}
}
And lastly the item where the lastMessage is actually displayed
export default class MessageNavigationItem extends Component {
render() {
const { hovered } = this.state;
const { user, selected, chat, isGroupChat, group, id } = this.props;
const { messages } = chat;
const item = isGroupChat ? group : user;
const lastMessage = _last(messages);
return (
<div>
{`${user.firstName} (${user.unit})`}
{lastMessage && lastMessage.content}
</div>
)
}
In the end it was an async setup issue.
In the action 'messages' are a sub-collection of the collection 'chats'.
To retrieve them it is an async operation.
When I returned a Promise for the messages of each chat and awaited for it before I run the success dispatch function, the messages are shown as expected.

How do I manage my array of children components' states?

I'm new to react, so forgive me. I'm having a problem understanding states, specifically those of children.
Purpose: I'm trying to create a form that a user can append more and more components -- in this case, images.
What happens: User appends 2 or more images. User tries to upload an image with UploadButton component, but both the images are the same. I believe this has to do with both appended children sharing the same state.
Question: How do I give each appended child its own image without affecting the other appended children?
class Page extends Component
constructor (props) {
super(props);
this.state = {
id: '',
numChildren: 0,
images: [],
}
this.onAddChild = this.onAddChild.bind(this);
}
showModal() {
this.setState({
numChildren: 0,
images: [],
});
}
renderModal()
const children = [];
//Here's my array of child components
for(var i = 0; i < this.state.numChildren; i += 1) {
children.push(<this.ChildComponent key={i} />);
}
return (
<ReactModal>
<this.ParentComponent addChild={this.onAddChild}>
{children}
</this.ParentComponent>
</ReactModal>
)
}
onAddChild = () => {
this.setState({
numChildren: this.state.numChildren + 1
})
}
ParentComponent = (props) => (
<div>
{props.children}
<Button onClick={props.addChild}>Add Item</Button>
</div>
);
ChildComponent = () => (
<div>
<UploadButton
storage="menus"
value={this.state.images}
onUploadComplete={uri => this.setState({images: uri})}
/>
</div>
);
}
Here's the code for UploadButton:
import React, { Component } from 'react';
import uuid from 'uuid';
import firebase from '../config/firebase';
class UploadButton extends Component {
constructor(props) {
super(props);
this.state = {
isUploading: false
}
}
handleClick() {
const input = document.createElement("INPUT");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/gif, image/jpeg, image/png");
input.addEventListener("change", ({target: {files: [file]}}) => this.uploadFile(file));
input.click();
}
uploadFile(file) {
console.log('F', file);
const id = uuid.v4();
this.setState({ isUploading: true })
const metadata = {
contentType: file.type
};
firebase.storage()
.ref('friends')
.child(id)
.put(file, metadata)
.then(({ downloadURL }) => {
this.setState({ isUploading: false })
console.log('Uploaded', downloadURL);
this.props.onUploadComplete(downloadURL);
})
.catch(e => this.setState({ isUploading: false }));
}
render() {
const {
props: {
value,
style = {},
className = "image-upload-button",
},
state: {
isUploading
}
} = this;
return (
<div
onClick={() => this.handleClick()}
className={className}
style={{
...style,
backgroundImage: `url("${this.props.value}")`,
}}>
{isUploading ? "UPLOADING..." : !value ? 'No image' : ''}
</div>
);
}
}
export default UploadButton;
I tried to exclude all unnecessary code not pertaining to my problem, but please, let me know if I need to show more.
EDIT: This is my attempt, it doesn't work:
//altered my children array to include a new prop
renderModal() {
const children = [];
for (var i = 0; i < this.state.numChildren; i += 1) {
children.push(<this.ChildComponent imageSelect={this.onImageSelect} key={i} />);
}
//...
};
//my attempt to assign value and pass selected image back to images array
ChildComponent = () => (
<div>
<UploadButton
storage="menus"
value={uri => this.props.onImageSelect(uri)} //my greenness is really apparent here
onUploadComplete={uri => this.setState({images: uri})}
/>
//...
</div>
);
//added this function to the class
onImageSelect(uri) {
var el = this.state.images.concat(uri);
this.setState({
images: el
})
}
I know I'm not accessing the child prop correctly. This is the most complexity I've dealt with so far. Thanks for your time.
When you write this.state in Child / Parent component, you are actually accessing the state of Page. Now, I would recommend that you pass in the index of the child to the Child like so
children.push(<this.ChildComponent key={i} index={i}/>)
so that each children deals with only its own image like so
ChildComponent = ({index}) => (
<div>
<UploadButton
storage="menus"
value={this.state.images[index]}
onUploadComplete={uri => {
let images = this.state.images.slice()
images[index] = uri
this.setState({images})
}}
/>
</div>
);

React/Redux Why does specific component update, when its sibling’s child component updates, though its state doesn’t change

Update
The sidedrawers state is apparently different, but value does not change...
Details:
There is a layout component, which takes in routes from react router as children.
Inside the layout component code, two child components are rendered, Toolbar, and sidedrawer, and a main section that contains this.props.children.
One of the routes renders a component called page. Page renders another component called graphContainer, and passes it a click event, which is applied to the graphContainer’s button that it renders.
How it works is, I grab the first eight graphs and show 4 of them. When the button is clicked, it decides to either show the next 4 or grab the next eight.
This whole thing uses redux. There’s a page state, authentication state, navigation state, and a graph state. The only partial state changing when the button is clicked, is the graphs.
However, both the GraphContainer updates along with the sidedrawer component. As far as I can tell, nothing in the sidedrawer component is changing, so it should not trigger an update.
In the redux page for navigation state, the switch hits the default, which just returns state.
The graph redux portion works just fine, updates accordingly.
My workaround was to implement a dontUpdate prop in the navigation reducer state. And then use shouldComponentUpdate to check that prop, because the shallow check that was done by default, say with pureComponent, was seeing a different state or prop.
tl;dr: Any ideas why the sidedrawer component keeps updating, even though, as far as I can tell, there’s no prop or state change?
Reducers
const graphReducer = (state = initialState, action) => {
...
case SHOW_NEXTFOUR:
console.log('SHOW NEXT FOUR', state);
return {
...state,
ttlShown: action.ttlShown
};
default:
return state;
}
};
const navReducer = (state = initialState, action) => {
...
default:
return {...state, dontUpdate: true};
}
};
Layout Component
class Layout extends Component {
...
handleSideBarOpen = () => {
this.props.onSidebarToggle();
}
render () {
return (
<Aux>
<Toolbar
isAuth={this.props.isAuthenticated}
drawerToggleClicked={this.handleSideBarOpen}
/>
<SideDrawer
open={this.props.sidebarOpen}
closed={this.props.onSidebarToggle}
/>
<main className={classes.Content}>
{this.props.children}
</main>
</Aux>
)
}
}
const mapStateToProps = ({ navigation, auth }) => {
const { sidebarOpen } = navigation;
const { token } = auth;
return {
sidebarOpen,
isAuthenticated: token !== null
};
};
const mapDispatchToProps = {
onSidebarToggle, getNavTree
};
export default connect(
mapStateToProps, mapDispatchToProps
)(Layout);
Sidedrawer Component
class sideDrawer extends Component {
state = {
popupMenuOpen: false
}
shouldComponentUpdate ( nextProps, nextState ) {
if(nextProps.dontUpdate)
return false;
else return true;
}
…
render() {
…
let navitems = [];
if(this.props.navData && !this.props.error) {
navitems = (
<NavigationItems
showClients={this.props.showClientsBtn}
navData={this.props.navData}
curClientid={this.props.curClientid}
curSiteid={this.props.curSiteid}
curDashid={this.props.curDashid}
curPageid={this.props.curPageid}
closeSidebar={this.props.closed}
onPageClick={this.handlePageClick}
onCSDClick={this.handleOpenPopupMenu}
/>
);
} else
navitems = <p>Problem Loading Tree</p>;
return (
<Aux>
<div className={attachedClasses.join(' ')}>
<div className={classes.Logo}>
<div className={classes.CloseWrapper}>
<Chip onClick={this.props.closed} className={classes.CloseChip}>X</Chip>
</div>
<div className={classes.CrumbWrapper}>
<Breadcrumbs
backBtn={this.handleBackClick}
handleCrumbClick={this.handleCrumbClick}
breadcrumbs={this.props.breadcrumbs}
/>
</div>
</div>
<nav>
{navitems}
<Popover
style={{width: "90%"}}
open={this.state.popupMenuOpen}
anchorEl={this.state.anchorEl}
anchorOrigin={{horizontal: 'middle', vertical: 'bottom'}}
targetOrigin={{horizontal: 'middle', vertical: 'top'}}
onRequestClose={this.handleClosePopupMenu}
>
<Menu
style={{width: "87%"}}>
{MIs}
</Menu>
</Popover>
</nav>
</div>
</Aux>
);
}
};
const mapStateToProps = ({ navigation }) => {
const { dontUpdate, clientid, breadcrumbs,currentPage, selectedClient, selectedSite, selectedDash, selectedPage, navigationData, sidebarOpen, navError } = navigation;
...
}
return {
dontUpdate,
clientid,
showClientsBtn,
navData,
curClientid,
curSiteid,
curDashid,
curPageid,
parentPageid,
sidebarOpen,
navError,
breadcrumbs,
currentPage
};
};
const mapDispatchToProps = {
getNavTree,
onPageSelected,
onSwitchCSD,
onPageRoute
};
export default withRouter(connect(
mapStateToProps, mapDispatchToProps
)(sideDrawer));
Page Component
class Page extends Component {
componentWillMount () {
this.props.getCurPage();
}
render () {
let content = null;
if(this.props.location.state && this.props.location.state.currentPage)
content = (<GraphContainer pageid={this.props.location.state.currentPage} />);
return this.props.location.state && this.props.location.state.currentPage ? (
<Aux>
<p>A PAGE!</p>
{content}
</Aux>
) : (<Redirect to="/" />);
}
}
const mapStateToProps = ({pages}) => {
const { clientid, curPage } = pages;
return {
clientid, curPage
};
};
const mapDispatchToProps = {
getSelectedPage, getCurPage
};
export default connect(
mapStateToProps, mapDispatchToProps
)(Page);
Graph Container
class GraphsContainer extends Component {
componentWillReceiveProps(newProps) {
if(this.props.pageid !== newProps.pageid)
this.props.getFirstEight(newProps.pageid);
}
componentDidMount() {
if(this.props.pageid)
this.props.getFirstEight(this.props.pageid);
}
handleNextClick = (event) => {
event.preventDefault();
this.props.getNextEight(this.props.pageid, this.props.lastNum, this.props.ttlShown);
}
render() {
let graphcards = null;
let disableNext = null;
if (this.props.lastNum >= this.props.ttl)
disableNext = true;
if(this.props.graphs && this.props.graphs.length > 0) {
graphcards = ...
}
return (
<div className={classes.Shell}>
{graphcards}
{this.props.lastNum < this.props.ttl ? (
<div className={classes.NavBtns}>
<RaisedButton disabled={disableNext} onClick={this.handleNextClick}>{'V'}</RaisedButton>
</div>
):null}
</div>
);
}
}
const mapStateToProps = ({pageGraphs}) => {
const { graphs, ttl, lastNum, ttlShown } = pageGraphs;
return {
graphs, ttl, lastNum, ttlShown
};
};
const mapDispatchToProps = {
getFirstEight, getNextEight
};
export default connect(
mapStateToProps, mapDispatchToProps
)(GraphsContainer);
Actions
export const getFirstEight = (pageid) => {
let str = ...;
return (dispatch) => {
axios.get( str )
.then( response => {
let data = {};
let graphs;
let ttl;
let newLastNum = 0;
if((typeof response.data !== 'undefined') && (response.data !== null)) {
data = {...response.data};
ttl = data.total;
if(ttl <= 8) {
graphs = [...data.graphs];
newLastNum = ttl;
} else {
graphs = [...data.graphs].slice(0,8);
newLastNum = 8;
}
}
dispatch({type: GET_FIRSTEIGHT, payload: {ttl,graphs, lastNum:newLastNum}});
} )
.catch( error => {
console.log('ERROR FETCHING NAV TREE', error);
dispatch({type: GET_FIRSTEIGHT, payload: {}});
} );
};
};
export const getNextEight = (pageid, lastNum, ttlShown) => {
let str = ...;
let newLastNum = 0;
return (dispatch) => {
if(ttlShown < lastNum) {
dispatch({type: SHOW_NEXTFOUR, ttlShown: ttlShown+4});
} else {
axios.get( str )
.then( response => {
// console.log('[RESPONSE]', response);
let data = {};
let graphs;
let ttl;
if((typeof response.data !== 'undefined') && (response.data !== null)) {
data = {...response.data};
ttl = data.total;
if(ttl <= (lastNum+8)) {
graphs = [...data.graphs].slice(lastNum);
newLastNum = ttl;
} else {
graphs = [...data.graphs].filter((el,index) => {
return (index > (lastNum-1)) && (index < (lastNum+8));
});
newLastNum = lastNum+8;
}
}
dispatch({type: GET_NEXTEIGHT, payload: {ttl,graphs, lastNum:newLastNum, ttlShown: ttlShown+4}});
} )
.catch( error => {
console.log('ERROR FETCHING NAV TREE', error);
dispatch({type: GET_NEXTEIGHT, payload: {}});
} );
}
};
};

Resources