Update children's state in higher parent component - reactjs

I need some help. I have a wrapper App component where I hold the state of the application, the App component renders a Router component. I need to set in every page of the application the changes that were made on the page. Every page has a next button to the next page using NavLinks.
I tried to pass down a handle change method but I can not get it to work, the handleChange doesn't get fired. Any help would be very much appreciated.
App:
export default class App extends React.Component {
state = {
categoriesWithSub: [],
currentPage: '',
ticket: {
departments: '',
request: '',
exactRequest: '',
attachments: '',
},
};
componentDidMount = async () => {
const categories = await getCategories();
const categoriesWithSub = await Promise.all(
categories.map(async category => {
const subCategories = await getSubCategories(category.id);
return { ...category, subCategories };
}),
);
this.setState({ categoriesWithSub });
};
makeHandleChange = () => {
alert('Test!');
/* this.setState({
ticket: { ...this.state.ticket, [pageName]: pageChanges },
}); */
};
render() {
const { categoriesWithSub } = this.state;
return <Router categoriesWithSub={categoriesWithSub} handleChange={this.makeHandleChange} />;
}
}
Router:
const routes = [
{
path: '/',
exact: true,
component: Home,
},
{
path: '/departments',
exact: true,
component: Departments,
},
{
path: '/request',
exact: true,
component: Request,
},
{
path: '/thank-you',
exact: true,
component: ThankYou,
},
{
path: '/success',
exact: true,
component: Success,
},
];
export default function Router(categoriesWithSub, handleChange) {
return (
<Switch>
{routes.map(({ path, exact, component: Component, layoutProps = {} }) => {
const WithLayout = ({ ...props }) => (
<Layout {...layoutProps}>
<Component {...props} />
</Layout>
);
return (
<Route
key={path}
path={path}
exact={exact}
render={() => <WithLayout categoriesWithSub={categoriesWithSub} handleChange={handleChange} />}
/>
);
})}
</Switch>
);
}
Child component:
export default class extends Component {
state = {
};
componentDidMount() {
this.props.handleChange(this.state);
}
render() {
const { categoriesWithSub } = this.props.categoriesWithSub;
return (
{categoriesWithSub &&
categoriesWithSub.map(item => (
<Tile
to="./request"
department={item.catName}
key={item.id}
/>
))}
</div>
);
}
}
export function Tile({ to, department }) {
return (
<NavLink to={to} css={tile}>
{department}
</NavLink>
);
}

This will not work
export default function Router(categoriesWithSub, handleChange)
That should be
export default function Router({categoriesWithSub, handleChange})
//or
export default function Router(props){
const {categoriesWithSub, handleChange} = props;
}

Related

Testing history.goback with testing-library and react

I'm trying to check the goback navigation in this component:
class BackMore extends Component {
render() {
return (
<div className="backMore">
<div className="back" onClick={ this.props.history.goBack } data-testid="go-back">
<FontAwesomeIcon icon={faArrowLeft} />
</div>
<span className="title">{ this.props.title }</span>
<More/>
</div>)
}
}
export default withRouter(BackMore)
I use testing-library and the recipe from page https://testing-library.com/docs/example-react-router
// test utils file
function renderWithRouter(
ui,
{
route = '/',
history = createMemoryHistory({ initialEntries: [route] }),
} = {}
) {
const Wrapper = ({ children }) => (
<Router history={history}>{children}</Router>
)
return {
...render(ui, { wrapper: Wrapper }),
// adding `history` to the returned utilities to allow us
// to reference it in our tests (just try to avoid using
// this to test implementation details).
history,
}
}
And this is my test:
test('Go back in the history', () =>{
const browserHistory = createMemoryHistory()
browserHistory.push('/my-learning');
const { history } = RenderWithRouter(<BackMore />, { route: ['my-learning'], history: browserHistory });
userEvent.click(screen.getByTestId(/go-back/i));
expect(history.location.pathname).toBe('/')
})
The history.location.pathname variable is 'my-learning' and it should be '/'
What is it wrong?

Pass a JSX element to storybook parameters in a custom build addon

I am building a custom Tab
import React from 'react';
import { addons, types } from '#storybook/addons';
import { AddonPanel } from '#storybook/components';
import { useParameter } from '#storybook/api';
export const ADDON_ID = 'storybook/principles';
export const PANEL_ID = `${ADDON_ID}/panel`;
export const PARAM_KEY = 'principles'; // to communicate from stories
const PanelContent = () => {
const { component: Component } = useParameter(PARAM_KEY, {});
if (!Component) {
return <p>Usage info is missing</p>;
}
return <Component />;
};
addons.register(ADDON_ID, api => {
addons.add(PANEL_ID, {
type: types.Panel,
title: 'Usage',
paramKey: PARAM_KEY,
render: ({ active, key }) => {
return (
<AddonPanel active={active} key={key}>
<PanelContent />
</AddonPanel>
);
},
});
});
& then using it in my stories like
storiesOf('Superman', module)
.addParameters({
component: Superman,
principles: {
component: <Anatomy />
},
})
.add('a story 1', () => <p>some data 1</p>)
.add('a story 2', () => <p>some data 2</p>)
The part where I try to pass in a JSX element like
principles: { component: <Anatomy /> }, // this does not work
principles: { component: 'i can pass in a string' }, // this does work
I get an error like below when I pass in a JSX element as a prop
How can I pass in a JSX element to storybook parameters?
Found a way:
regiter.js
import { deserialize } from 'react-serialize'; //<-- this allows json to jsx conversion
// ...constants definitions
...
const Explanation = () => {
const Explanations = useParameter(PARAM_KEY, null);
const { storyId } = useStorybookState();
const storyKey = storyId.split('--')?.[1];
const ExplanationContent = useMemo(() => {
if (storyKey && Explanations?.[storyKey])
return () => deserialize(JSON.parse(Explanations?.[storyKey]));
return () => <>No extra explanation provided for the selected story</>;
}, [storyKey, Explanations?.[storyKey]]);
return (
<div style={{ margin: 16 }}>
<ExplanationContent />
</div>
);
};
addons.register(ADDON_ID, () => {
addons.add(PANEL_ID, {
type: types.TAB,
title: ADDON_TITLE,
route: ({ storyId, refId }) =>
refId
? `/${ADDON_PATH}/${refId}_${storyId}`
: `/${ADDON_PATH}/${storyId}`,
match: ({ viewMode }) => viewMode === ADDON_PATH,
render: ({ active }) => (active ? <Explanation /> : null),
});
});
and when declaring the parameter:
{
parameters:{
component: serialize(<p>Hello world</p>)
}
}

How to add page number to the URL

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} />

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.

Prevent page reload when rendering ui fabric react nav component

I'm stuck trying to get the ui-fabric Nav component working with react-router-dom v4+. My solution "works", but the whole page is rerendered instead of just the NavSelection component. After some research i realize i need to do a e.preventDefault() somewhere, but i can't figure out where to add it.
Main Page:
export const Home = () => {
return (
<div className="ms-Grid-row">
<div className="ms-Grid-col ms-u-sm6 ms-u-md4 ms-u-lg2">
<Navbar />
</div>
<div className="ms-Grid-col ms-u-sm6 ms-u-md8 ms-u-lg10">
<NavSelection />
</div>
</div>
);
}
Navbar:
const navGroups = [
{
links: [
{ name: 'Name1', url: '/Name1', key: '#Name1' },
{ name: 'Name2', url: '/Name2', key: '#Name2' }
]
}
];
export class Navbar extends React.Component<any, any> {
constructor(props: INavProps) {
super(props);
this.state = {
selectedNavKey: '#Name1'
};
}
public componentDidMount() {
window.addEventListener('hashchange', (e) => {
this.setState({ selectedNavKey: document.location.hash || '#' });
});
}
public render(): JSX.Element {
const { selectedNavKey } = this.state;
return (
<Nav
selectedKey={selectedNavKey}
groups={navGroups}
/>
);
}
}
NavSelection:
export const NavSelection = () => {
return (
<div>
<Route path="/Name1" component={Component1} />
<Route path="/Name2" component={Component2} />
</div>
);
}
Any help is greatly appreciated
Edit: I've tried to put it inside componentDidMount like this:
public componentDidMount() {
window.addEventListener('hashchange', (e) => {
e.preventDefault();
this.setState({ selectedNavKey: document.location.hash || '#' });
});
}
That does not work.
Use the HashRouter instead of the BrowserRouter.
Example:
Router:
...
import { Switch, Route, Redirect, HashRouter } from 'react-router-dom'
...
export const Router: React.FunctionComponent = () => {
// persisted to localStorage
const navActiveItem = useSelector(selectNavActiveItem)
return (
<Suspense fallback={<LargeSpinner />}>
<HashRouter>
<Switch>
<Route exact path="/" render={() => (
<Redirect to={navActiveItem.url} />
)}/>
<Route exact path="/dashboard/overview" component={Overview} />
<Route exact path="/dashboard/progress" component={Progress} />
<Route exact path="/dashboard/issues" component={Issues} />
...
</Switch>
</HashRouter>
</Suspense>
)
}
Navigation:
...
const navLinkGroups: INavLinkGroup[] = [
{
name: 'Dashboard',
expandAriaLabel: 'Expand Dashboard section',
collapseAriaLabel: 'Collapse Dashboard section',
links: [
{
key: 'DashboardOverview',
name: 'Overview',
icon: 'BIDashboard',
url: '#/dashboard/overview',
},
{
key: 'DashboardProgress',
name: 'Progress',
icon: 'TimelineProgress',
url: '#/dashboard/progress',
},
{
key: 'DashboardIssues',
name: 'Issues',
icon: 'ShieldAlert',
url: '#/dashboard/issues',
},
],
},
...
export const Navigation: React.FunctionComponent = () => {
const navActiveItem = useSelector(selectNavActiveItem)
const dispatch = useDispatch()
const onLinkClick = (ev?: React.MouseEvent<HTMLElement>, item?: INavLink) => {
dispatch(setNavActiveItem(item || { name: '', url: '/' }))
}
return (
<Stack tokens={stackTokens} styles={stackStyles}>
<Nav
styles={navStyles}
ariaLabel="Navigation"
groups={navLinkGroups}
onLinkClick={onLinkClick}
initialSelectedKey={navActiveItem.key}
/>
</Stack>
)
}
I'm guessing you are using Microsoft's https://developer.microsoft.com/en-us/fabric#/components/nav#Variants
In that case you need to specify the callback on the nav item. Usually it's anti-pattern to use things like window.addEventListener in react.
This would look something like.
export class Navbar extends React.Component<any, any> {
constructor(props: INavProps) {
super(props);
this.state = {
selectedNavKey: '#Name1'
};
}
public handleNavClick(event, { key, url }) {
// You also need to manually update the router something like
history.push(url);
this.setState({ selectedNavKey: key });
}
public render(): JSX.Element {
const { selectedNavKey } = this.state;
return (
<Nav
selectedKey={selectedNavKey}
groups={{
links: [
{ name: 'Name1', url: '/Name1', key: '#Name1', onClick: this.handleNavClick },
{ name: 'Name2', url: '/Name2', key: '#Name2', onClick: this.handleNavClick }
]
}}
/>
);
}
}
To prevent page refresh, call event.preventDefault() inside Nav component's onLinkClick event handler:
<Nav onLinkClick={linkClickHandler} selectedKey={selectedKey} />
function linkClickHandler(event,{key, url}){
event.preventDefault();
setSelectedKey(key);
console.log(url);
}

Resources