child component does not re-render when props change - reactjs

Problem:
The first page of my app shows list of Sale Invoices.
I select one which runs a function taking id of the clicked invoice as argument and gets its details via API call.
Customer component rendered from SaleInvoice has a input box (Typeahead component) which is supposed to show customerName passed down from SaleInvoice but does not do so correctly. It is sometimes blank and when I go back to the first page (list of Sale Invoices) and select other Sale Invoice, the customerName of the previous Sale Invoice. I checked the console log (see the line after <React.Fragment> in Customer component) and I can see the correct values of customerName in the state shown by reducers.
Initially, SaleInvoice was a stateful component but had no state object. I made it stateful to fetch my data via API in componentWillMount. Due to the above problem, I tried this:
added customerName in state in SaleInvoice
changed this.props.customerName to this.state.customerName in Customer props
used
getDerivedStateFromProps()
which says I cannot use componentWillMount
Also, tried shouldComponentUpdate and some other stuff.
Nothing works. Kindly help. If more code needs to be posted, please let me know.
relevant slice of reducer
case actionTypes.INVOICE_BY_ID_SUCCESS:
let customerData = action.payload[0];
let saleInvoiceData = action.payload[1];
let newState = Object.assign({}, state);
newState.loading = false;
newState.error = null;
newState.customerInfo = {
...state.customerInfo,
id: customerData.id,
place: customerData.place,
addressLineOne: customerData.address_line_one,
};
newState.saleInvoiceId = saleInvoiceData.id;
newState.customerName = saleInvoiceData.customer_name;
newState.serialNumber = saleInvoiceData.serial_number;
newState.amountBeforeFreight = saleInvoiceData.amount_before_freight;
newState.freight = saleInvoiceData.freight;
newState.amountAfterFreight = saleInvoiceData.amount_after_freight;
return newState;
SaleInvoiceContainer.js (excluding imports)
const mapStateToProps = (state, ownProps) => {
console.log(`ownProps ${ownProps}`);
console.log(ownProps);
return {
customerLoading: state.saleInvoiceReducer.customerLoading,
customerError: state.saleInvoiceReducer.customerError,
productError: state.lineItemsReducer.error,
productLoading: state.lineItemsReducer.loading,
saleInvoiceError: state.saleInvoiceReducer.error,
saleInvoiceLoading: state.lineItemsReducer.error,
saleInvoiceId: state.saleInvoiceReducer.saleInvoiceId,
customerData: state.saleInvoiceReducer.customerData, // data of all customers
productData: state.lineItemsReducer.productData, // data of all products
customerInfo: state.saleInvoiceReducer.customerInfo, // data of current customer
addingCustomer: state.saleInvoiceReducer.addingCustomer, // modal show/hide
customerName: state.saleInvoiceReducer.customerName, // input field name
grandTotal: subTotalSelector(state),
};
};
const mapDispatchToProps = (dispatch, ownProps) => {
return {
fetchCustomer: () => dispatch(fetchCustomer()),
fetchProduct: () => dispatch(fetchProduct()),
getInvoiceById: () =>
dispatch(getInvoiceById(ownProps.location.state.id)),
onBlurCustomerName: event => dispatch(onBlurCustomerName(event)),
stopAddingCustomer: () => dispatch(stopAddingCustomer()),
};
};
const SaleInvoiceContainer = connect(
mapStateToProps,
mapDispatchToProps,
)(SaleInvoice);
SaleInvoice.js (excluding imports)
class SaleInvoice extends React.Component {
state = {
customerName: '',
};
componentWillMount() {
// if api is called from here, state will not update when api updates
// props change cause re-render
this.props.getInvoiceById();
this.props.fetchCustomer();
this.props.fetchProduct();
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.customeName !== prevState.customerName) {
return {customerName: nextProps.customerName};
} else return null;
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.customerName !== this.state.customerName) {
let customerName = this.state.customerName;
//Perform some operation here
this.setState({customerName});
}
}
render() {
console.log(this.props);
let ui = this.props.customerError ? (
<p>Customers failed to load!</p>
) : (
<Spinner />
);
let printLink = '/sale-invoice/' + this.props.saleInvoiceId + '/print';
let sui = this.props.productError ? (
<p>Products failed to load!</p>
) : (
<Spinner />
);
if (
!this.props.customerLoading &&
!this.props.customerError &&
!this.props.error
) {
console.log('Customers have been loaded');
ui = (
<React.Fragment>
<Modal
show={this.props.addingCustomer}
modalClosed={this.props.stopAddingCustomer}
customerData={this.props.customerData}
name={this.state.customerName}
/>
<div className={classes.mainContainerTitle}>
{console.log(this.props.grandTotal)}
<h5 className={classes.pageTitle}>Sale Invoice</h5>
<NavLink className={classes.NavLink} to={printLink}>
Print
</NavLink>
{/*<button>Print</button>*/}
</div>
<rs.Container
fluid
className={[classes.mainContainer, classes.containerFluid].join(
'',
)}>
<rs.Row className={classes.firstRow}>
<Customer
customerData={this.props.customerData}
onBlurCustomerName={this.props.onBlurCustomerName}
customerInfo={this.props.customerInfo}
customerName={this.state.customerName}
/>
<SaleInvoiceSummary grandTotal={this.props.grandTotal} />
</rs.Row>
</rs.Container>
</React.Fragment>
);
}
if (
!this.props.productLoading &&
!this.props.productError &&
!this.props.error
) {
console.log('Products have been loaded');
sui = (
<React.Fragment>
<rs.Container fluid className={classes.gridContainer}>
<LineItemsContainer />
</rs.Container>
</React.Fragment>
);
}
return (
<React.Fragment>
{ui}
{sui}
</React.Fragment>
);
}
}
Customer.js (exluding imports)
const Customer = props => {
function _renderMenuItemChildren(option, props, index) {
return [
<Highlighter key="name" search={props.text}>
{option.name}
</Highlighter>,
<div key="place">
<small>Place: {option.place}</small>
</div>,
];
}
return (
<React.Fragment>
{console.log(props.customerName)}
<rs.Card col="sm-4" className={classes.firstCard}>
<rs.CardHeader className={classes.cardHeader}>
Customer Details
</rs.CardHeader>
<rs.CardBody className={classes.cardBodySaleInvoice}>
<rs.Label>Name</rs.Label>
<React.Fragment>
<Typeahead
className={classes.customerTypeahead}
defaultInputValue={props.customerName}
allowNew={true}
newSelectionPrefix="Add New: "
disabled={false}
labelKey="name" // this determines what array key value to show
multiple={false}
options={props.customerData}
placeholder="Choose a customer..."
onBlur={event => props.onBlurCustomerName(event)}
renderMenuItemChildren={_renderMenuItemChildren}
/>
<rs.FormGroup />
</React.Fragment>
<div className={classes.customerCardBody}>
<rs.Label>Address</rs.Label>
<div className={classes.address}>
{props.customerInfo.addressLineOne}
<br />
{props.customerInfo.addressLineTwo}
<br />
{props.customerInfo.address_line_three}
<br />
{props.customerInfo.contact_no_one}
<br />
{props.customerInfo.gst_number}
<br />
<button>Edit</button>
</div>
</div>
</rs.CardBody>
</rs.Card>
</React.Fragment>
);
};
(PS: I am new to React, additional comments/criticism regarding the code will be helpful)

I added a html input component and found that it worked correctly.
The issue was with the Typeahead component.
https://github.com/fmoo/react-typeahead/issues/74#issuecomment-112552406
Also, now using react-select instead of Typeahead and that works correctly too.

Related

React, passed state is not changing in other component

I started with react and have a small problem
export default function TestComponent3({ typeId}) {
console.log('CHANGING HERE', { typeId});
const handleClick = () => {
console.log('NOT CHANGING HERE', { typeId});
console.log('NOT CHANGING HERE EITHER', typeId);
};
return (
<>
<Spectrum.Button variant="secondary" onClick={handleClick}>
CHANGING HERE {typeId}
</Spectrum.Button>
</>
);
}
In the other component the state of 'myProp' is changing by the UI dropdown.
Spectrum.Button content changes dynamically but console logs are stuck on default state option.
I probably messed something up and and It's easy fix.
EDIT://
Sibling with a dropdown
export default function TypeForm({ typeId, setTypeId }) {
return (
<div>
<Spectrum.Dropdown className="dropdown" placeholder="Choose type...">
<Spectrum.Menu onChange={setTypeId} slot="options">
{Types.map(type => { //types 0,1,2,3,4,5,6,7,8
return (
<Spectrum.MenuItem selected={typeId === type.id ? true : null} key={type.id} className="jsontest">
{type.name}
</Spectrum.MenuItem>
);
})}
</Spectrum.Menu>
</Spectrum.Dropdown>
)}
</div>
);
}
And Parent
export default function Form() {
const [typeId, setTypeId] = useState(0);
const setTypeIdFunc = e => {
setTypeId(e.target.selectedIndex);
};
return (
<TestComponent3 typeId={typeId} />
<TypeForm
typeId={typeId}
setTypeId={setTypeIdFunc}
/>
)
The props are passed down first from the parent component of TestComponent3 during the first render.
This means you would have to send handleClick from one "higher" component. Or use a shared state-like context provider.
I fixed it by changing Spectrum.Button to normal button.

React not rendering list after the state is changed

I am creating a simple tracker which records all the activities done. It is my first project in react. I have created three state one for storing all the items(name of state is list), one for pending items(name of state is pending) , one for completed items(name of state is completed). The items have a button which when clicked marks it into done state and vice-versa. It is completely rendering items for main list. But for other two its not rendering. When I am checking with react developer tools, it is working fine, i.e. it is adding to pending list or completed list as it should. But it is not compiling them on screen. list is already filled with items. I have added all the code for just in case.
function Filters(props){
const [completed, setCompleted] = useState([]);
const [pending, setPending] = useState([]);
const [state, setState] = useState("None");
const [list,setList] = useState([]);
function viewState(){
setState("View-all");
}
//it is getting the clicked item id and marking it complete in main list
function markComplete(id){
list.map((items,index)=>{
if(index===id){
if(items.done===true)
items.done = false;
else{
items.done=true;
}
}
})
}
//i am simply scanning the main list and the items which are pending will be added to this list. //this happens whenever the person click on pending button
function pendingState(){
setState("pending-all");
setPending([]);
list.map(items=>{
if(items.done!==true){
setPending(prev=>{
return [...prev,items];
})
}
})
}
function completedState(){
setState("completed-all");
setCompleted([]);
list.map(items=>{
if(items.done===true){
setCompleted(prev=>{
return [...prev,items];
})
}
})
}
return (
<div>
<div className="input-section">
<Note setList={setList} />
</div>
<button type="button" onClick={viewState} >View All</button>
<button type="button" onClick={completedState}>Completed</button>
<button type="button" onClick={pendingState}>Pending</button>
<div>
{
(()=>{
if(state==="View-all")
{
return (
<div>
<h1>Working {completed}</h1>
{(list).map((items,index)=>
{
return (
<Excercise
key={index}
id={index}
title={items.name}
list={props.list}
setList={props.setList}
content={items.desp}
markComplete={markComplete}
/>
)
})}
</div>
)
}
else if(state==="completed-all")
{
return (
<div>
{completed.map((items,index)=>{
<Excercise
key={index}
id={index}
title={items.name}
list={props.list}
setList={props.setList}
content={items.desp}
markComplete={markComplete}
/>
})}
</div>
)
}
})()
}
</div>
</div>);
}
Kindly help. Thank you.
Hi #DREW
The function code :
function markComplete(id){
setList(lists=>{
lists.map(item=>{
return item.id===id ?{...item,done: !item.done} : (item);})
}
)
}
When I am using it instead of
const markComplete = (id) => {
setList((list) =>
list.map((item) =>
item.id === id
? {
...item,
done: !item.done
}
: item
)
);
};
it is showing, "Cannot read properties of undefined (reading 'filter')"
arent the both same. If not, what am I doing wrong. Sorry for bugging so many times, I have just started with react.
I think you've overcomplicated things a bit. You only need one array to store the exercises in, the "pending" and "completed" states are easily derived from the list state and the state filter state value.
Issues
markComplete callback is mutating the list state. When updating the list state not only is a new array reference necessary, but also new element object references are necessary for the elements that are being updated.
Uses poor boolean comparisons to set a boolean value. You can either toggle a boolean or set the value to the result of a boolean expression.
Use the viewState, pendingState, and completedState handlers to simply set the filter value, and then derive the computed state when rendering by adding an inline filter function.
Use the exercise id property as a React key and as the property used for toggling the completed (done) state.
Solution
function Filters(props) {
const [state, setState] = useState("None");
const [list, setList] = useState([
...
]);
function viewState() {
setState("View-all");
}
function pendingState() {
setState("pending-all");
}
function completedState() {
setState("completed-all");
}
const markComplete = (id) => {
setList((list) =>
list.map((item) =>
item.id === id
? {
...item,
done: !item.done
}
: item
)
);
};
return (
<div>
<div className="input-section">
<Note setList={setList} />
</div>
<button type="button" onClick={viewState}>
View All
</button>
<button type="button" onClick={completedState}>
Completed
</button>
<button type="button" onClick={pendingState}>
Pending
</button>
<div>
{list
.filter((item) => {
if (state === "pending-all") {
return !item.done;
} else if (state === "completed-all") {
return item.done;
}
return true;
})
.map((item) => (
<Excercise
key={item.id}
id={item.id}
done={item.done}
title={item.name}
content={item.desp}
markComplete={markComplete}
/>
))}
</div>
</div>
);
}
try to add dependecies in useEffect
in this function you are mutating a state, so in order to do so you need to use the setState function, in this case, it will be setList().
function markComplete(id){
list.map((items,index)=>{
if(index===id){
if(items.done===true)
items.done = false;
else{
items.done=true;
}
}
})
}
So a better way to implement this function could be, and remember, everything time you need to update a state you don't want to change the state directly, instead, you should make a copy and set the state to that copy
function markComplete(id){
const newList = [...list];
newList.map((items,index)=>{
if(index===id){
if(items.done===true)
items.done = false;
else{
items.done=true;
}
}
}
setList(newList)
}
The reason of your app not updating is because when your state changes react is not re-rendering it again.
so use useEffect, there are many hooks which can be used as per requirement.
try putting this line of code
useEffect( ( ) => {
console.log( 'Check console' );
}, [ dependency_here ] );
in dependency_here try adding state, completed, pending one by one and see the result.
You can also add multiple dependencies like [ state, pending, etc.. ];
Try on your own you'll understand it faster.
Hope hint will help you!

I have rendered two components and whenever I type something input field input field loses its focus

Here is my component into which component is rendered
const BasicInfo=({
product,
handleChange,
handleTags,
productData,
deleteTags,
categories,
customProduct,
handleCustomChange,
setCustomProduct,
})=>{
function increasenumber() {
var hello = numberoffields;
hello = hello + 1;
setNumberoffields(hello);
setCustomProduct((arr) => [...arr, { typeoffield: "", label: "" }]);
}
const DisplayCustomInput = () => {
if (product.customCategory.includes(product.category)) {
return (
<div>
<div>
<button onClick={increasenumber}>Add input field</button>
</div>
<DisplayInputs
numberoffields={numberoffields}
customProduct={customProduct}
handleCustomChange={handleCustomChange}
/>
</div>
);
} else {
return null;
}
};
return (
<DisplayCustomInput />
)
}
My DisplayInputs looks like
export default function DisplayInputs({
numberoffields,
customProduct,
handleCustomChange,
}) {
const rows = [];
for (let index = 0; index < numberoffields; index++) {
console.log(index);
rows.push(
<div key={index}>
<label htmlFor="label">
Enter label for input field for accepting data
</label>
<input
value={customProduct[index] ? customProduct[index].label : ""}
onChange={handleCustomChange(index, "label")}
id="label"
/>
</div>
)
}
return rows
}
Whenever a character if fed to the input field it loses focus and if I use DisplayCustomInput as jsx instead of component then on increasing numberoffields the whole page gets reload.
You can use a ref in React. You initial her in the child and you call something like nomRef.current.focus on useEffect in the parent, useEffect with dependency the value that the input change.
One thing more :
onChange={() => handleCustomChange(index, "label")}
and not onChange={handleCustomChange(index, "label")}
(because without () => you execute the function on mount.
If you need about ref : https://fr.reactjs.org/docs/refs-and-the-dom.html
Say me if it's good for you

Filter state that is set from onClick as data- in react

Two main issues.
(1) onClick needs to update two items in my state
(2) I need to filter state #2 to count the number of times a string appears and render it if it equals the length of state#1
More Detail:
I am mapping through my objects and rendering a button for each. I need the onClick to setState of two different attributes so i pass value={item.item} to update state selectedItems , and data-matches={item.matches} to update state of matchesList.
-- note that item.item is a string and item.matches is an array of strings.
When I call onClick to update the state, the value works fine, but the data-matches creates a weird object which i cant iterate over and this is why thats a problem...
I need to map through the data-matches state and count each instance of a string, if the count of that particular string is equal to the length of selectedItems state, then I need to render that.
If this is confusing to you, it's because I am completely lost and new to this. Maybe its worth mentioning that my props are coming from Redux..
Example of some objects for reference
{
'item': 'apple',
'matches': ['beef', 'bacon', 'cheese', 'carrot'],
},
{
'item': 'carrot',
'matches': ['apple', 'bacon', 'goat'],
},
export class Items extends Component {
constructor(props) {
super(props);
this.state = {
selectedItems: [],
firstStep: true,
matchesList: []
};
this.selectItems = this.selectItems.bind(this);
this.filterMatches = this.filterMatches.bind(this);
}
selectItems(event) {
this.setState({
selectedItems: [...this.state.selectedItems, event.target.value],
firstStep: false,
matchesList: [...this.state.matchesList, event.target.dataset.matches]
});
}
Below, in the col 'All", I am mapping through my state selectedItems which is an array, so I can grab the actual object from props which is the same name. I then map over the matches array of that object, and grab the actual object that has the same name as the matches...
in the col 'Cross Referenced", I am mapping over the state updated from the onClick. This is where I need to filter.. I'll show you my attempt at that after this code block.
render() {
return (
<Fragment>
<div className="col">
<div className="row">
<div className="col-5">
<h1>All</h1>
{this.state.selectedItems.map(selected =>
this.props.items.map(item =>
item.item == selected
? item.matches.map(match =>
this.props.items.map(item =>
item.item == match ? (
<div>
<p>{item.item}</p>
<button
key={item.item}
value={item.item}
data-matches={item.matches}
onClick={this.selectItems}
>
Select
</button>
</div>
) : null
)
)
: null
)
)}
</div>
<div className="col-5">
<h1>Cross Referenced</h1>
{this.state.matchesList.map(matches => (
<p>{matches}</p>
))}
</div>
</div>
</div>
)}
</div>
</Fragment>
);
}
}
My attempt at filtering, even though the matchesList is not right.
filterMatches(matchesList, selectedItems) {
let arr1 = matchesList;
let arr2 = selectedItems;
let obj = arr1.reduce((op, inp) => {
let key = inp;
op[key] = op[key] || 0;
op[key]++;
return op;
}, {});
let final = Object.keys(obj).filter(key => {
return obj[key] === arr2.length;
});
return final;
}
in the render
<div className="col-5">
<h1>cross referenced</h1>{this.state.matchesList.filter(this.filterMatches(this.state.matchesList, this.state.selectedItems)).map(matches => (
<p>{matches}</p>
))}
</div>
</div>
I tried out the data-matches thing, and it looks to me like event.target.dataset.matches was coming out to be a string (Not an array of strings, one big CSV string). Try doing it the following way:
class YourComponent extends React.Component {
state = {...};
selectItems = (item, matches) => {
this.setState({
selectedItems: [...this.state.selectedItems, item],
firstStep: false,
matchesList: [...this.state.matchesList, ...matches]
});
}
render() {
return (
{...}
{this.props.items.map(item => (
<button
key={item.item}
value={item.item}
onClick={() => this.selectItems(item.item, item.matches)}
>
Select
</button>
))}
{...}
);
}
}
Actually I think I see what your issue is, the filter is working fine and it's already giving you your array of ["bacon"]. Try getting rid of the outer filter, I don't see a point to it being there.
const YourComponent = props => (
{...}
{this.filterMatches(this.state.matchesList, this.state.selectedItems).map(matches => (
<p>{matches}</p>
))}
{...}
);

mapStateToProps value lags behind getState() by a keystroke

I am using redux's connect() on App.js and then passing down a dispatcher to a child. The child also receives the state variable as a prop.
The issue is a log statement on the state variable through the prop mapping lags 1 digit behind what is entered. Here are 2 log statements showing this:
SelectDonation.js:19 getState value {donation_amount: "345"}
SelectDonation.js:20 mapstatetoprops value 34
Here is the child component SelectDonation.js:
handleInputChange = inputEvent => {
this.props.dispatch_set_donation_amount(inputEvent.target.value);
console.log("getState value", store.getState());
console.log("mapstatetoprops value", this.props.donation_amount)
};
render() {
return (
<React.Fragment>
<Form>
<input
type='number'
placeholder='Custom Amount'
name='donation_amount'
id='custom_amount'
onChange={(e) => this.handleInputChange(e)}
/>
<Button
primary
onClick={(event) => {
event.preventDefault();
this.props.dispatchChangeCheckoutStep(checkoutSteps.paymentDetails);
console.log(store.getState().checkoutStep)
}}>Next Step
</Button>
</Form>
</React.Fragment>
)
}
}
App.js:
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<Modal
trigger={<Button color='purple'>Donate</Button>}
size='small'
>
{this.props.checkoutStep === checkoutSteps.selectDonation &&
<SelectDonation
dispatch_set_donation_amount = {this.props.dispatch_set_donation_amount}
dispatchChangeCheckoutStep={this.props.dispatchChangeCheckoutStep}
{...this.props} // passes down all the state
/>
}
{this.props.checkoutStep === checkoutSteps.paymentDetails && <PaymentDetails
redux_donation_amount={this.props.redux_donation_amount}
{...this.props}
/>
}
{this.props.checkoutStep === checkoutSteps.referrals && <Referrals dispatchUpdateStateData={this.props.dispatchUpdateStateData} />}
</Modal>
</header>
</div>
);
}
}
const map_state_to_props = (state) => {
return {
log_prop : state.log_to_console,
donation_amount : state.donation_amount,
checkoutStep : state.checkoutStep,
}
};
const map_dispatch_to_props = (dispatch, own_props) => {
return {
dispatch_set_donation_amount : amount => dispatch(set_donation_amount(amount)),
dispatchChangeCheckoutStep : newStep => dispatch(changeCheckoutStep(newStep)),
dispatchUpdateStateData : (stateData, stateVariable) => (dispatch(updateStateData(stateData, stateVariable)))
}
};
//connecting redux
export const AppWrapped = connect(map_state_to_props, map_dispatch_to_props)(App);
It looks correct to me but obviously I am neglecting something. What is causing the mapStateToProps variable to be one keystroke behind persistently?
The component has not updated yet. You have called an action to be dispatched to your reducer, but not waited for the component to receive the new props. You can see this by either pushing the second log into a setTimeout (not recommended)
this.props.dispatch_set_donation_amount(inputEvent.target.value);
console.log("getState value", store.getState());
setTimeout(() => console.log("mapstatetoprops value", this.props.donation_amount));
or by using a middleware such as thunk and returning a Promise from your action creator.
this.props.dispatch_set_donation_amount(inputEvent.target.value).then(x => console.log("mapstatetoprops value", x));
console.log("getState value", store.getState());
Because React has not had a chance to update the component. Your own code is still executing, so the rest of the component lifecycle has not executed yet, and therefore your component still has its existing props.
Also, please don't access the store directly in your components. One of the main purposes of connect is to handle that for you.

Resources