Cannot pass value to parent component in React.Js - reactjs

I'm strugelling with that one.
I'm trying to build a calendar using react-calendar package. In one place of my app I need to have an access to the calendar month's value. I'm calling the components property onActiveDateChange which I found in docs of the component (react-calendar). It is indeed a callback so I'm trying to use it as my chance to extract month value and send it up to the parent component. But that does not work, not only the value is not changed as expected, but the calendar stops working. Do you know what is causing that? I've tried also with setting a state in the callback but the same results, value is not correct.
Here's my chunk of code:
import React, { Component } from 'react';
import Calendar from 'react-calendar';
import ChooseHour from './ChooseHour';
import { connect } from 'react-redux';
import * as actions from '../actions';
class Calendario extends Component {
state = { showHours: false,}
onChange = date => this.setState({
date }, () => {
const { chosenRoom } = this.props;
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const fullDate = `${year}/${month}/${day}`;
const roomAndDayObj = {fullDate, chosenRoom};
this.props.sendRoomAndDay(roomAndDayObj);
}
)
onClickDay(e) {
this.setState({ showHours: true });
}
passActiveDate(activeDate) {
const monthAfterPress = activeDate.getMonth() + 1;
console.log(monthAfterPress);
// this is weird in my opinion, I can log it and it works as expected,
// when I'm firing the next line the values are incorrect
// this.props.activeMonthToPass(monthAfterPress); //this line makes a problem
}
render() {
const { booked } = this.props;
return (
<div>
<div className="calendarsCont">
<Calendar
onChange={this.onChange}
onClickDay={(e) => this.onClickDay(e)}
onActiveDateChange={({ activeStartDate }) => this.passActiveDate(activeStartDate)}
value={this.state.date}
locale="pl-PL"
tileDisabled={({date, view }) =>
date.getDate()===15 && date.getMonth()===6 && date.getFullYear()===2018}
/>
</div>
<div>
{this.state.showHours ?
<ChooseHour chosenDay={this.state.date} chosenRoom={this.props.chosenRoom}/> :
null}
</div>
</div>
)
}
}
export default connect (null, actions)(Calendario);

Related

How do you rerender a React component when an object's property is updated?

I have an object in my React application which is getting updated from another system. I'd like to display the properties of this object in a component, such that it updates in real time.
The object is tracked in my state manager, Jotai. I know that Jotai will only re-render my component when the actual object itself changes, not its properties. I'm not sure if that is possible.
Here is a sample that demonstrates my issue:
import React from "react";
import { Provider, atom, useAtom } from "jotai";
const myObject = { number: 0 };
const myObjectAtom = atom(myObject);
const myObjectPropertyAtom = atom((get) => {
const obj = get(myObjectAtom)
return obj.number
});
const ObjectDisplay = () => {
const [myObject] = useAtom(myObjectAtom);
const [myObjectProperty] = useAtom(myObjectPropertyAtom);
const forceUpdate = React.useState()[1].bind(null, {});
return (
<div>
{/* This doesn't update when the object updates */}
<p>{myObject.number}</p>
{/* This doesn't seem to work at all. */}
<p>{myObjectProperty}</p>
{/* I know you shouldn't do this, its just for demo */}
<button onClick={forceUpdate}>Force Update</button>
</div>
);
};
const App = () => {
// Update the object's property
setInterval(() => {
myObject.number += 0.1;
}, 100);
return (
<Provider>
<ObjectDisplay />
</Provider>
);
};
export default App;
Sandbox
you can use useEffect for this.
useEffect(()=> {
// code
}, [myObject.number])

input value not updating when mutating state

While creating a little project for learning purposes I have come across an issue with the updating of the input value. This is the component (I have tried to reduce it to a minimum).
function TipSelector({selections, onTipChanged}: {selections: TipSelectorItem[], onTipChanged?:(tipPercent:number)=>void}) {
const [controls, setControls] = useState<any>([]);
const [tip, setTip] = useState<string>("0");
function customTipChanged(percent: string) {
setTip(percent);
}
//Build controls
function buildControls()
{
let controlList: any[] = [];
controlList.push(<input className={styles.input} value={tip.toString()} onChange={(event)=> {customTipChanged(event.target.value)}}></input>);
setControls(controlList);
}
useEffect(()=>{
console.log("TipSelector: useEffect");
buildControls();
return ()=> {
console.log("unmounts");
}
},[])
console.log("TipSelector: Render -> "+tip);
return (
<div className={styles.tipSelector}>
<span className={globalStyles.label}>Select Tip %</span>
<div className={styles.btnContainer}>
{
controls
}
</div>
</div>
);
}
If I move the creation of the input directly into the return() statement the value is updated properly.
I'd move your inputs out of that component, and let them manage their own state out of the TipSelector.
See:
https://codesandbox.io/s/naughty-http-d38w9
e.g.:
import { useState, useEffect } from "react";
import CustomInput from "./Input";
function TipSelector({ selections, onTipChanged }) {
const [controls, setControls] = useState([]);
//Build controls
function buildControls() {
let controlList = [];
controlList.push(<CustomInput />);
controlList.push(<CustomInput />);
setControls(controlList);
}
useEffect(() => {
buildControls();
return () => {
console.log("unmounts");
};
}, []);
return (
<div>
<span>Select Tip %</span>
<div>{controls}</div>
</div>
);
}
export default TipSelector;
import { useState, useEffect } from "react";
function CustomInput() {
const [tip, setTip] = useState("0");
function customTipChanged(percent) {
setTip(percent);
}
return (
<input
value={tip.toString()}
onChange={(event) => {
customTipChanged(event.target.value);
}}
></input>
);
}
export default CustomInput;
You are only calling buildControls once, where the <input ... gets its value only that single time.
Whenever React re-renders your component (because e.g. some state changes), your {controls} will tell React to render that original <input ... with the old value.
I'm not sure why you are storing your controls in a state variable? There's no need for that, and as you noticed, it complicates things a lot. You would basically require a renderControls() function too that you would replace {controls} with.

How can I change the state of one React component based on the value of its sibling?

I'm attempting to build a form in React that has some auto-filling features. I'm trying to create the ability to 'lock' the value of LossOver50k to either 'Yes' or 'No' based on the ReplacementCost value to prevent input errors. The code I have right now is
import React, { useState } from "react";
import ReplacementCost from "./ReplacementCost";
import LossOver50K from "./LossOver50K";
const LossAmount = () => {
const [replacementCost, setReplacementCost] = useState("");
const [highValue, setHighValue] = useState("No");
const handleChange = (newReplacementCost) => {
setReplacementCost(newReplacementCost);
if (replacementCost >= 50000) {
setHighValue("Yes");
}
};
// const changeHighValue = (newHighValue) => {
// setHighValue(newHighValue);
// };
return (
<div>
<ReplacementCost value={replacementCost} onChange={handleChange} />
<LossOver50K value={highValue} />
</div>
);
};
export default LossAmount;
My commented piece was working as an onChange handler on the LossOver50k component but I'm trying to manipulate the value of LossOver50k if the value of Replacement Cost is equal to or greater than 50,000. I have each of these components successfully updating their state when I input the values directly by passing the props down to the child components. Any ideas?
TIA!
Your condition should be with the most updated value and not with a staled one (as setState is async):
const handleChange = (newReplacementCost) => {
setReplacementCost(newReplacementCost);
if (newReplacementCost >= 50000) {
setHighValue("Yes");
}
};

Getting 'objects are not a valid React child' error when attempting to render date in React web app

I am attempting to pull a string integer from my database, convert it into a number, then transform that number into a date.
However, I keep getting the following error every time I load my page when live.
Objects are not valid as a React child (found: Mon Dec 14 2020
10:48:11 GMT+0000 (GMT)). If you meant to render a collection of
children, use an array instead.
The date variables are working exactly as expected, I just want to now render them in my app, which is where the problem is occurring.
Can someone point out where I'm going wrong here?
Here's my full component....
import React, { Component } from 'react';
import axios from 'axios';
import './individual-debts.css';
import IndividualDebtCard from '../individual-debts/individual-debt-card';
class IndividualDebts extends Component {
constructor(props) {
super(props)
this.state = {
debts: []
}
}
componentDidMount() {
axios.get("/api/fetch/fetchDebtCards")
.then((response) => {
this.setState({
debts: response.data,
fetchInProgress: false
})
})
})
}
render() {
const fetchDebts = this.state.debts.map (debt => {
const dayCurrent = new Date();
const dayFromDB = parseInt(debt.date);
const dayFromDBDate = new Date(new Date().setDate(dayFromDB));
const variableDayFromDB = new Date(new Date().setDate(dayFromDB));
const dayFromDBDatePlusOne = variableDayFromDB.setMonth(variableDayFromDB.getMonth()+1)
let dayToRender = ''
if (dayCurrent < dayFromDBDate) {
dayToRender = dayFromDBDate
console.log(dayToRender)
} else {
dayToRender = variableDayFromDB
console.log(dayToRender)
}
return (
<IndividualDebtCard key={debt._id}
monthly={debt.monthly}
repayment={dayToRender} />
)
})
return (
<div>
{fetchDebts}
</div>
)
}
}
export default IndividualDebts;
Any advice here would be really appreciated! Thank you.
Date instance is an object in JavaScript so it cant be passed as a children prop in react (since it accepts only a string or an element)
To solve this issue simply convert the Date instance to an ISOString (or any other format you like)
<IndividualDebtCard
key={debt._id}
monthly={debt.monthly}
repayment={dayToRender.toISOString()} />
If you are trying to render your date in a <p> (for example), it won't work as dates are Objects.
You could write a function to transform it to a string or use a lib like date-fns.
Here is a go at it:
export const DisplayDate = (date: Date | string | number): string => {
if (date) {
const dateAsDate = new Date(date);
const day =
dateAsDate.getDate() < 10
? `0${dateAsDate.getDate()}`
: dateAsDate.getDate();
const month =
dateAsDate.getMonth() < 9
? `0${dateAsDate.getMonth() + 1}`
: `${dateAsDate.getMonth() + 1}`;
const year = dateAsDate.getFullYear();
return `${day}-${month}-${year}`;
}
return '-';
};

UseEffect causes infinite loop with swipeable routes

I am using the https://www.npmjs.com/package/react-swipeable-routes library to set up some swipeable views in my React app.
I have a custom context that contains a dynamic list of views that need to be rendered as children of the swipeable router, and I have added two buttons for a 'next' and 'previous' view for desktop users.
Now I am stuck on how to get the next and previous item from the array of modules.
I thought to fix it with a custom context and custom hook, but when using that I am getting stuck in an infinite loop.
My custom hook:
import { useContext } from 'react';
import { RootContext } from '../context/root-context';
const useShow = () => {
const [state, setState] = useContext(RootContext);
const setModules = (modules) => {
setState((currentState) => ({
...currentState,
modules,
}));
};
const setActiveModule = (currentModule) => {
// here is the magic. we get the currentModule, so we know which module is visible on the screen
// with this info, we can determine what the previous and next modules are
const index = state.modules.findIndex((module) => module.id === currentModule.id);
// if we are on first item, then there is no previous
let previous = index - 1;
if (previous < 0) {
previous = 0;
}
// if we are on last item, then there is no next
let next = index + 1;
if (next > state.modules.length - 1) {
next = state.modules.length - 1;
}
// update the state. this will trigger every component listening to the previous and next values
setState((currentState) => ({
...currentState,
previous: state.modules[previous].id,
next: state.modules[next].id,
}));
};
return {
modules: state.modules,
setActiveModule,
setModules,
previous: state.previous,
next: state.next,
};
};
export default useShow;
My custom context:
import React, { useState } from 'react';
export const RootContext = React.createContext([{}, () => {}]);
export default (props) => {
const [state, setState] = useState({});
return (
<RootContext.Provider value={[state, setState]}>
{props.children}
</RootContext.Provider>
);
};
and here the part where it goes wrong, in my Content.js
import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
import SwipeableRoutes from 'react-swipeable-routes';
import useShow from '../../hooks/useShow';
import NavButton from '../NavButton';
// for this demo we just have one single module component
// when we have real data, there will be a VoteModule and CommentModule at least
// there are 2 important object given to the props; module and match
// module comes from us, match comes from swipeable views library
const ModuleComponent = ({ module, match }) => {
// we need this function from the custom hook
const { setActiveModule } = useShow();
// if this view is active (match.type === 'full') then we tell the show hook that
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match]);
return (
<div style={{ height: 300, backgroundColor: module.title }}>{module.title}</div>
);
};
const Content = () => {
const { modules, previousModule, nextModule } = useShow();
// this is a safety measure, to make sure we don't start rendering stuff when there are no modules yet
if (!modules) {
return <div>Loading...</div>;
}
// this determines which component needs to be rendered for each module
// when we have real data we will switch on module.type or something similar
const getComponentForModule = (module) => {
// this is needed to get both the module and match objects inside the component
// the module object is provided by us and the match object comes from swipeable routes
const ModuleComponentWithProps = (props) => (
<ModuleComponent module={module} {...props} />
);
return ModuleComponentWithProps;
};
// this renders all the modules
// because we return early if there are no modules, we can be sure that here the modules array is always existing
const renderModules = () => (
modules.map((module) => (
<Route
path={`/${module.id}`}
key={module.id}
component={getComponentForModule(module)}
defaultParams={module}
/>
))
);
return (
<div className="content">
<div>
<SwipeableRoutes>
{renderModules()}
</SwipeableRoutes>
<NavButton type="previous" to={previousModule} />
<NavButton type="next" to={nextModule} />
</div>
</div>
);
};
export default Content;
For sake of completion, also my NavButton.js :
import React from 'react';
import { NavLink } from 'react-router-dom';
const NavButton = ({ type, to }) => {
const iconClassName = ['fa'];
if (type === 'next') {
iconClassName.push('fa-arrow-right');
} else {
iconClassName.push('fa-arrow-left');
}
return (
<div className="">
<NavLink className="nav-link-button" to={`/${to}`}>
<i className={iconClassName.join(' ')} />
</NavLink>
</div>
);
};
export default NavButton;
In Content.js there is this part:
// if this view is active (match.type === 'full') then we tell the show hook that
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match]);
which is causing the infinite loop. If I comment out the setActiveModule call, then the infinite loop is gone, but of course then I also won't have the desired outcome.
I am sure I am doing something wrong in either the usage of useEffect and/or the custom hook I have created, but I just can't figure out what it is.
Any help is much appreciated
I think it's the problem with the way you are using the component in the Route.
Try using:
<Route
path={`/${module.id}`}
key={module.id}
component={() => getComponentForModule(module)}
defaultParams={module}
/>
EDIT:
I have a feeling that it's because of your HOC.
Can you try
component={ModuleComponent}
defaultParams={module}
And get the module from the match object.
const ModuleComponent = ({ match }) => {
const {type, module} = match;
const { setActiveModule } = useShow();
useEffect(() => {
if (type === 'full') {
setActiveModule(module);
}
},[module, setActiveModule]);
match is an object and evaluated in the useEffect will always cause the code to be executed. Track match.type instead. Also you need to track the module there. If that's an object, you'll need to wrap it in a deep compare hook: https://github.com/kentcdodds/use-deep-compare-effect
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match.type, module]);

Resources