I am trying to have 3 buttons where if one is in an active state, the other 2 will automatically be inactive.
if (isActive === "true") {
setActive2("false")
setActive3("false")
}
if (isActive2 === "true") {
setActive("false")
setActive3("false")
}
if (isActive3 === "true") {
setActive("false")
setActive2("false")
}
I'm aware there's probably a better way of doing this and this is a brute force option, and I'm open to your suggestions.
I have tried putting this block of code in a function and running it whenever the buttons are clicked, but that is giving me the previous state instead of the current state.
So I was suggested to use the useEffect hook.
useEffect(() => {
if (isActive === "true") {
setActive2("false")
setActive3("false")
}
if (isActive2 === "true") {
setActive("false")
setActive3("false")
}
if (isActive3 === "true") {
setActive("false")
setActive2("false")
}
}, [isActive, isActive2, isActive3]);
However this is giving me the same issue, where the previous state is being applied.
I am for sure doing something very wrong with this hook (i have never used it before).
I have a codesandbox with all my code here
Have modified only the onChange handler in an efficient way without touching JSX much and have worked on your set-up only. CodeSandBox Link Checkbox-Selection
Some Major changes that I did are as follows:
Instead of setting seperate state to each button, I have used a single object with 3 keys isActive, isActive2 and isActive3.
const [btnStatus, setBtnStatus] = useState({
isActive: true,
isActive2: true,
isActive3: true
});
Your Handler looks something like this now.
const addPizza = (e) => {
setPizzaSize(e.target.name);
setStartPrice(parseInt(e.target.value));
const currentActive = e.target.id;
if (currentActive === "isActive") {
setBtnStatus({ isActive: true, isActive2: false, isActive3: false });
console.log("1");
}
if (currentActive === "isActive2") {
setBtnStatus({ isActive: false, isActive2: true, isActive3: false });
console.log("2");
}
if (currentActive === "isActive3") {
setBtnStatus({ isActive: false, isActive2: false, isActive3: true });
console.log("3");
}
console.log(btnStatus);
};
In your JSX each button will look like this, with own ids to track the status of button.
<button
name="Extra Large"
className={
btnStatus.isActive3
? "button btn fourth"
: "button btn fourthActive"
}
value="20"
onClick={addPizza}
id="isActive3"
>
Extra large
</button>
And here you go. All working nicely with the same code :)
I have update the code a little bit, you can create seprate constants and use them to reduce the code and also, to keep the active state use a single state only.
https://codesandbox.io/s/gracious-franklin-m8wkx?file=/src/CYO.js:0-4147
import React, { useState, useEffect } from "react";
import ButtonClickable from "./button";
import ButtonClickable2 from "./button2";
import { burgerSize, vegToppings, nonvegToppings } from "./const/size";
import "./index.css";
const CYO = () => {
const [pizzaSize, setPizzaSize] = useState("Choose your Pizza Size");
const [activeSize, setActiveSize] = useState(burgerSize.MEDIUM);
const [toppings, setToppings] = useState([]);
const [startPrice, setStartPrice] = useState(0);
const addPizza = (e) => {
setPizzaSize(e.target.name);
setStartPrice(parseInt(e.target.value));
};
const CheckSize = () => {
if (pizzaSize === "Choose your Pizza Size") {
alert("You must choose a pizza size");
} else if (toppings.length === 0) {
alert("Are you sure you don't want toppings?");
} else {
alert("Sorry, this isn't a real pizza place.");
}
};
const ToppingPlusMinus = (e) => {
const { value } = e.target;
const position = toppings.indexOf(value);
if (position !== -1) {
return removeTopping(value);
}
return addTopping(value);
};
const removeTopping = (value) => {
// We need to filter out the value from the array and return the expected new value
setToppings(toppings.filter((topping) => topping !== value));
//handleToggle();
};
const addTopping = (value) => {
setToppings([...toppings, value]);
// handleToggle();
};
let toppingPrice = toppings.length * 1.5;
let price = startPrice + toppingPrice;
return (
<div className="container CYO">
<h2 className="text-center white">Create your own pizza</h2>
<div className="row">
<div className="col-sm-8">
<div className="">
<img
src="./pizza.png"
className="img-fluid pizza"
alt="Pizza"
></img>
</div>
<h3 className="white">{pizzaSize}</h3>
<p className="white">
Your Toppings: <br />
<div className="col-lg-12">
{toppings
.filter((x) => x.name !== "")
.map((toppings) => (
<img
src={toppings}
alt="topping"
width="100px"
height="100px"
></img>
))}
</div>{" "}
</p>
</div>
<div className="col-sm-4">
<h3 className="white">Pizza size</h3>
{Object.values(burgerSize).map((value) => (
<button
name={value}
className={
activeSize !== value
? "button btn fourth"
: "button btn fourthActive"
}
value="10"
onClick={(event) => {
addPizza(event);
setActiveSize(value);
}}
>
{value}
</button>
))}
<br />
<h3 className="white">Toppings</h3>
<p className="white">Toppings are $1.50 each</p>
<div className="topping-wrapper">
<h4 className="white">Meats</h4>
{nonvegToppings.map(({ name, image }) => (
<ButtonClickable
onClick={(event) => {
ToppingPlusMinus(event);
}}
name={name}
value={image}
/>
))}
<h4 className="white">Veggies</h4>
{vegToppings.map(({ name, image }) => (
<ButtonClickable2
onClick={(event) => {
ToppingPlusMinus(event);
}}
name={name}
value={image}
/>
))}
</div>
</div>
<div className="pricefooter">
<p className="price">Price: ${price}</p>
</div>
<div className="pricefooter2">
<button className="checkout button btn fourth" onClick={CheckSize}>
Checkout
</button>
</div>
</div>
</div>
);
};
export default CYO;
Related
I can't seem to find an answer to this that I can understand
I have tabs and a tab block, which is a clone of this basic tab block plugin. I'm trying to add the ability to select an "icon" for each tab block.
I'm close. Using the MediaUpload component, I'm able to see the file I've selected under the activeTab object, but it doesn't update the parent block attribute, so I can't reference the icon_url attribute.
tab/edit.js
const Edit = ({ attributes, setAttributes, clientId }) => {
const { uid, activeTab } = attributes;
useEffect(() => {
if (!uid) {
setAttributes({ uid: clientId });
}
}, []);
const display = activeTab === uid ? "block" : "none";
const ALLOWED_MEDIA_TYPES = ["image", "svg"];
const setTabIcon = (icon_url) => {
const parentBlock = select("core/block-editor").getBlock(clientId);
dispatch("core/block-editor").updateBlockAttributes(
parentBlock.clientId,
{
...attributes,
icon_url,
}
);
};
return (
<div {...useBlockProps()}>
<InspectorControls>
<div>
<MediaUpload
allowedTypes={ALLOWED_MEDIA_TYPES}
onSelect={(media) => setTabIcon(media.url)}
render={({ open }) => (
<button onClick={open}>Open Media Library</button>
)}
/>
</div>
</InspectorControls>
<div className={"guten-tab-panel"} style={{ display }}>
<InnerBlocks
allowedBlocks={["core/heading", "core/paragraph"]}
renderAppender={() => <InnerBlocks.ButtonBlockAppender />}
/>
</div>
</div>
);
};
export default Edit;
I would first think that using setAttributes here would also update the parent, but this only updates setActive in the child block. It doesn't keep the change.
In tabs.js, I'm trying to reference tab.icon_url. icon_url doesn't exist, only uid and title
tabs/tabs.js
const Edit = ({ attributes, setAttributes, clientId }) => {
const { tabs, activeTab } = attributes;
const blockProps = useBlockProps({
className: `${useBlockProps().className} guten-tab-wrapper`,
});
const setActiveTab = (uid) => {
setAttributes({ activeTab: uid });
const parentBlock = select("core/block-editor").getBlock(clientId);
parentBlock.innerBlocks.forEach((innerBlock) => {
dispatch("core/block-editor").updateBlockAttributes(
innerBlock.clientId,
{
activeTab: uid,
}
);
});
};
const addNewTab = () => {
const tab = createBlock("ahsan03/tab");
const position = tabs.length;
dispatch("core/block-editor").insertBlock(tab, position, clientId);
setAttributes({
tabs: [
...tabs,
{
uid: tab.clientId,
title: `Tab ${tabs.length + 1}`,
icon_url: "",
},
],
});
setActiveTab(tab.clientId);
};
const tabTitleChange = (newValue) => {
setAttributes({
tabs: [
...tabs.map((tab) => {
return tab.uid === activeTab
? {
...tab,
title: newValue,
}
: tab;
}),
],
});
};
useEffect(() => {
if (tabs.length && !activeTab) {
setActiveTab(tabs[0].uid);
}
}, [tabs]);
return (
<>
<div {...blockProps}>
<div className={"guten-tabs-nav"}>
{tabs.map((tab) => {
return (
<div
key={tab.uid}
className={"guten-tab-item"}
role="tab"
tabIndex="0"
onClick={() => setActiveTab(tab.uid)}
>
<div
className={`guten-tab-link${
tab.uid === activeTab
? " is-active"
: ""
}`}
>
<img src={tab.icon_url} alt="" />
{console.log("tabs tab", {
tab,
})}
<RichText
tagName="div"
value={tab.title}
onChange={tabTitleChange}
/>
</div>
</div>
);
})}
<Button
variant={"primary"}
icon={"plus"}
onClick={addNewTab}
>
{__("", "gtt")}
</Button>
</div>
<div className={"guten-tab-content"}>
<InnerBlocks
allowedBlocks={["ahsan03/tab"]}
renderAppender={false}
/>
</div>
</div>
</>
);
};
export default Edit;
How can I fix this so uploading an image is in the parent block attributes?
Here's an updated setTabIcon function that I think is closer to what I need, I'm just not sure what to do after fetching the parentBlock.
const setTabIcon = (icon_url) => {
const parentBlockIds =
select("core/block-editor").getBlockParents(clientId);
parentBlockIds.forEach((parentBlockId) => {
const parentBlock = select("core/block-editor").getBlock(parentBlockId);
console.log({ parentBlock });
});
};
Make a state variable in the parent component, and then pass that state and setState to the child as props, and then from the child component, you can update the state in the parent component.
I was able to fix this by moving setTabIcon and the MediaFile component to the parent block.
Would love suggestions on how to improve this code.
I have 3 buttons and i want to add a class active when i click one of them. If i click another button i want to add active to that button and remove it from the previous one. i tried some ways but failed. i'm new in react .
function Filters() {
const [isActive, setIsActive] = useState(false);
const dispatch = useDispatch();
const handleClick = (e) => {
if (e.target.value === "Hot") {
dispatch(fetchHotImages());
} else if (e.target.value === "Top") {
dispatch(fetchTopImages());
} else {
dispatch(fetchUserImages());
}
};
return (
<div className={styles.filtersContainer}>
<div className={styles.filtersLeft}>
<h1 className={styles.filtersTitle}>Explore Gallery</h1>
</div>
<div className={styles.filtersRight}>
<div className={styles.trending}>
<button className={styles.actions} onClick={handleClick}>
Hot
</button>
<button className={styles.actions} onClick={handleClick}>
Top
</button>
<button className={styles.actions} onClick={handleClick}>
User
</button>
</div>
</div>
</div>
);
}
To add the active class using the above code, I'm guessing you're looking to add the active class to the buttons. All you'd have to add a style ternary in this form and change the state:
function Filters() {
const [isActive, setIsActive] = useState({
hotActive: false,
topActive: false,
userActive: false,
});
const dispatch = useDispatch();
const handleClick = (value) => {
if (value === "Hot") {
dispatch(fetchHotImages());
setIsActive({hotActive: true, topActive: false, userActive: false})
} else if (value === "Top") {
setIsActive({hotActive: false, topActive: true, userActive: false})
dispatch(fetchTopImages());
} else {
setIsActive({hotActive: false, topActive: false, userActive: true})
dispatch(fetchUserImages());
}
};
return (
<div className={styles.filtersContainer}>
<div className={styles.filtersLeft}>
<h1 className={styles.filtersTitle}>Explore Gallery</h1>
</div>
<div className={styles.filtersRight}>
<div className={styles.trending}>
<button className={isActive.hotActive ? {styles.actions} : {}} onClick={()=>handleClick("Hot")}>
Hot
</button>
<button className={isActive.topActive ? {styles.actions} : {}} onClick={()=>handleClick("Top")}>
Top
</button>
<button className={isActive.userActive ? {styles.actions} : {}} onClick={()=>handleClick("User")}>
User
</button>
</div>
</div>
</div>
);
}
A better way would be to make use of CSS, but this should work for you.
If i switch between Transfer to Beneficiary and All Beneficiary tab the Select options get duplicated but if i change the second useEffect hook dependency to allBeneficiary.current instead of beneficiaries presently there, Select options doesnt duplicate, but the options are not rendered on the first render until I switch to All Beneficiary tab and back to Transfer Beneficiary
Below is the Transfer to Beneficiary code
// Hooks and Contexts
import React, { useState, useContext, useEffect, useRef } from "react";
import { TransferPointsContext } from "../../../../../context/TransferPoints";
import { LoaderContext } from "../../../../../context/Loading";
// Components
import TransferSummary from "../../../../common/modals/TransferSummary";
import Loading from "../../../../features/Loader/Loading";
// UI
import swal from "sweetalert";
import toastr from "toastr";
import Select from "react-select";
import "./css/transfer-points.css";
import { nanoid }from 'nanoid'
function TransferPoints() {
const [showTransferSummary, setShowTransferSummary] = useState(false);
const [transferSummaryData, setTransferSummaryData] = useState(false);
const [showBeneficiaryDataPage, setShowBeneficiaryDataPage] = useState(false);
const [checked, setChecked] = useState(false);
const [ options, setOptions ] = useState([])
const {
verifyCardNumber,
verifyCardState,
getBeneficiaryList,
beneficiaries, //list of beneficiaries from API
hideBeneficiaryDataPage,
setInputs,
inputs,
} = useContext(TransferPointsContext);
const { loading } = useContext(LoaderContext);
toastr.options.progressBar = true;
toastr.options = {
toastClass: "alert",
iconClasses: {
error: "alert-error",
info: "alert-info",
success: "alert-success",
warning: "alert-warning",
},
};
const allBeneficiaries = useRef([]);
useEffect(() => {
getBeneficiaryList();
}, [allBeneficiaries]);
useEffect(() => {
if (beneficiaries.data !== null) {
if (
beneficiaries.data.status === 0 &&
beneficiaries.data.success === false
) {
toastr.error("Failed to fetch user beneficiaries!", "error", {
iconClass: "toast-error",
});
console.log("beneficiaries", beneficiaries.data);
} else if (
beneficiaries.data.status === 1 &&
beneficiaries.data.success === true
) {
console.log('All beneficiary ', allBeneficiaries.current)
beneficiaries.data.data.forEach((beneficiary) => {
console.log('For each ', beneficiary)
allBeneficiaries.current.unshift({
value: beneficiary.membership_number,
label: `${beneficiary.first_name} ${beneficiary.last_name == null ? '' : beneficiary.last_name}`,
});
});
console.log('LENGTH ', allBeneficiaries.current.length)
}
}
}, [beneficiaries]);
useEffect(() => {
if (hideBeneficiaryDataPage) {
setShowBeneficiaryDataPage(false);
}
}, [hideBeneficiaryDataPage]);
const beneficiaryData = useRef({});
useEffect(() => {
if (verifyCardState.data !== null) {
if (
verifyCardState.data.status === 1 &&
verifyCardState.data.success === true
) {
setShowBeneficiaryDataPage(true);
beneficiaryData.current = {
name: `${verifyCardState.data.data.first_name} ${verifyCardState.data.data.last_name == null ? '' : verifyCardState.data.data.last_name}`,
};
toastr.success("Membership Id Validated!", "Success", {
iconClass: "toast-success",
});
return;
}
if (
verifyCardState.data.status === 0 &&
verifyCardState.data.success === false
) {
if (verifyCardState.data.message && !verifyCardState.data.data) {
toastr.error(verifyCardState.data.message, "Validation failed!", {
iconClass: "toast-error",
});
setShowBeneficiaryDataPage(false);
return;
}
setShowBeneficiaryDataPage(false);
const errorMessages = verifyCardState.data.data;
for (const error in errorMessages) {
toastr.error(errorMessages[error], "Validation Error!", {
iconClass: "toast-error",
});
}
return;
}
}
}, [verifyCardState]);
const handleSearchInput = (event) => {
const card_number = event.value;
setInputs((inputs) => ({
...inputs,
card_number,
}));
verifyCardNumber(card_number);
};
const proceedToTransfer = () => {
const amount = document.getElementById("amount").value;
if (amount.trim().length === 0) {
swal({
title: "Oops!",
text: `Amount field cannot be empty`,
icon: "error",
button: "Ok",
});
return;
}
setShowTransferSummary(!showTransferSummary);
setTransferSummaryData({
amount,
name: beneficiaryData.current.name,
membership_id: inputs.card_number,
save_beneficiary: (beneficiaryData.current.save_beneficiary == 1) ? 1 : 0,
});
};
const handleInputChange = (event) => {
event.persist();
setInputs((inputs) => ({
...inputs,
[event.target.name]: event.target.value,
}));
};
const handleChange = (event) => {
event.persist();
setChecked(event.target.checked);
const save_beneficiary = event.target.checked === true ? 1 : 0;
beneficiaryData.current.save_beneficiary = save_beneficiary;
};
console.log('LENGTH ', allBeneficiaries.current)
// console.log('current beneficiaries ', (allBeneficiaries.current) )
// console.log('CHECKED ', beneficiaryData.current.save_beneficiary )
// let id = nanoid()
return (
<div>
{/* {loading ? <Loading /> : ""} */}
<form action="#">
<div className="row">
<div className="col-md-12">
<div className="form-group">
<label htmlFor="acc-email">Select Beneficiary </label>
<Select
onChange={handleSearchInput}
className="basic-single"
classNamePrefix="select"
isClearable="true"
isSearchable="true"
name="beneficiary_card_number"
defaultValue="Select"
options={allBeneficiaries.current} //THIS RETURNS DUPLICATED VALUE ON NAVIGATING TO ALL BENEFICIARIES AND BACK
/>
</div>
<h6 class="mt-3 heading-border border-0">OR</h6>
<div className="row align-items-center justify-content-between">
<div className="col-md-8">
<label htmlFor="card_number">Enter Membership Id</label>
<input
type="text"
className="form-control"
name="card_number"
onChange={handleInputChange}
value={inputs.card_number}
/>
</div>
<div className=" col-4 " style={{marginTop: '30px', paddingLeft: '10px', textAlign: 'end',}}>
<button
onClick={() => verifyCardNumber(inputs.card_number)}
type="button"
className="btn-lg btn btn-primary"
>
Validate Id
</button>
</div>
</div>
</div>
{showBeneficiaryDataPage === true ? (
<div className="col-sm-12">
<h6 class="mt-3 heading-border border-0"></h6>
<div className="row">
<div className="col-md-6">
<div className="form-group">
<label htmlFor="acc-name">Name</label>
<input
type="text"
className="form-control"
id="acc-name"
required
disabled
name="acc-name"
value={beneficiaryData.current.name}
/>
</div>
</div>
<div className="col-md-6">
<div className="form-group">
<label htmlFor="acc-lastname">Membership Id</label>
<input
type="text"
className="form-control"
id="acc-lastname"
required
disabled
name="acc-lastname"
value={inputs.card_number}
/>
</div>
</div>
</div>
<div className="row">
<div className="col-md-12">
<div className="form-group">
<label htmlFor="acc-lastname">Amount</label>
<input
type="text"
className="form-control"
id="amount"
required
name="amount"
onChange={handleInputChange}
value={inputs.amount}
/>
</div>
</div>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={checked}
name="save_beneficiary"
onChange={handleChange}
/>
<span className="ml-3 font-weight-bold terms-condition">
Save Beneficiary
</span>
</div>
<div className="mb-2"></div>
<div className="form-footer">
<div className="col-md-12 d-flex justify-content-center">
<button
onClick={() => proceedToTransfer()}
type="button"
className="btn-lg w-50 btn btn-primary"
>
Proceed
</button>
</div>
</div>
</div>
) : (
""
)}
</div>
</form>
{showTransferSummary === true ? (
<TransferSummary data={transferSummaryData} setShowTransferSummary={setShowTransferSummary} setShowBeneficiaryDataPage={setShowBeneficiaryDataPage}/>
) : (
""
)}
</div>
);
}
export default React.memo(TransferPoints);
This is the code for All Beneficiaries
import React, { useState, useContext, useEffect, useRef } from "react";
import "./css/transfer-points.css";
import { TransferPointsContext } from "../../../../../context/TransferPoints";
import Loading from "../../../../features/Loader/Loading";
import swal from "sweetalert";
import { LoaderContext } from "../../../../../context/Loading";
import toastr from "toastr";
import axios from 'axios'
import { ToastContainer, toast } from 'react-toastify';
function ShowAllBeneficiariesPage() {
const [ data, setData ] = useState([])
const {
getBeneficiaryList,
beneficiaries,
removeBeneficiary,
state,
} = useContext(TransferPointsContext);
const { loading } = useContext(LoaderContext);
toastr.options.progressBar = true;
toastr.options = {
toastClass: "alert",
iconClasses: {
error: "alert-error",
info: "alert-info",
success: "alert-success",
warning: "alert-warning",
},
};
const handleDelete = (id) => {
swal({
title: "Are you sure you want to remove beneficiary?",
text: "You won't be able to revert this!",
icon: "warning",
buttons: ["Cancel", "Proceed!"],
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
}).then((result) => {
if (result === true) {
removeBeneficiary(id);
console.log('ID OF RECEIVER ', id)
const newBeneficiary = data.filter(add => add.id !== id)
setData(newBeneficiary)
toastr.success("Beneficiary Removed !", "Success", {
iconClass: "toast-success",
});
}
});
};
const fetchData = () => {
axios.get(`user/beneficiaries`)
.then( res => setData(res.data.data ))
}
useEffect(() => {
fetchData()
}, [])
return (
<div>
{/* {loading ? <Loading /> : ""} */}
{ data.length === 0 ?
(
<div style={{textAlign: 'center'}}>No beneficiaries found</div>
)
:
(
<div className="col-sm-12">
{data.map((item) => {
console.log('Beneficiary Data ', data)
return (
<p className="mb-1 p-4 beneficiary-list">
{item.first_name} {item.last_name} - {item.membership_number}
<i
style={{ cursor: "pointer" }}
onClick={() => handleDelete(item.id)}
class="float-right fas fa-trash"
></i>{" "}
</p>
);
})}
</div>
)}
</div>
);
}
export default (ShowAllBeneficiariesPage);
I can't completely fix your issue, because I'd need more context and time, but I've found some issues on your code.
Never ever ever, have something in your render code that has a reference to a useRef variable. When a useRef value changes, react will completely ignore it and will not update your component. Use setState for those.
It sounds like your allBeneficiaries instead of being a ref or a state it's just derived state: It looks like it's a derived value from beneficiaries. In this case, you don't need to use any hook, just declare it as a const (e.g. const allBeneficiaries = getBeneficiaries(beneficiaries)). If you have performance issues, then consider using useMemo, but it should not be needed.
Never use a useRef as a dependency value in a useEffect - Same thing, react doesn't care about ref values, so you'll have unexpected behaviour there (effects retriggering when it shouldn't, effects not triggering when they should)
Try to avoid useEffect as much as posible. It should only be used for specific cases, such as fetching something from a server or manipulating the dom. For the rest of them, it's just problematic, best avoided.
Using allBeneficiaries (or any other ref object) as a dependency for a hook won't help you at all. The ref object's identity will never change over the lifetime of a component.
If you want to run an effect/... when the value boxed within the allBeneficiaries ref changes, the dependency will need to be allBeneficiaries.current.
Beside that, there's no good reason to use a ref for allBeneficiaries. Since it affects rendering, you will want to save it as a state atom (useState).
Why does ReactJS remove the last element when the array is different after removing the middle element when using array.splice?
This is my code. I am using React-Redux.
const reducerNotesAndLogin = (state = initialState, action) => {
var tableNotes = "notities";
var tableCategories = "categories";
switch(action.type){
case "CATEGORY_REMOVE":
// Remove the category
var newCategories = state.categories;
console.log("state.categories", state.categories);
console.log("before: ", {newCategories});
var index = 0;
for(var i = 0; i < newCategories.length; i++){
if(newCategories[i].id === action.payload.categoryId){
newCategories.splice(i, 1);
index = i;
i--;
}
}
console.log("after: ", {newCategories});
state = {
...state,
categories: newCategories
}
break;
default:
break;
}
return state;
}
export default reducerNotesAndLogin;
Output below (I deleted the middle element. My web app always removes the last element of the categories (but not from the array).
Step 1: Initial state
Step 2: Remove middle item, expecting the middle item to be removed.
Step 3: Confusion
Why is the array correct, but the view incorrect? I am updating the state.categories correctly right?
This is my render code (as is - without filtering away any other code that mihgt be important)
CategoriesBody:
import React from 'react';
import { connect } from 'react-redux';
import CategoryItem from './CategoryItem';
import Button from './../../Button';
import store from '../../../redux/store-index';
class CategoriesBody extends React.Component {
render(){
return (
<div>
<ul className="list--notes">
{this.props.categories.map((category) => {
if(category.id === undefined){ // No categories
return <li>No categories</li>
} else {
return (
<div>
<CategoryItem category={category} />
<div className="mb-small hidden-sm hidden-md hidden-lg"> </div>
</div>
);
}
})}
</ul>
</div>
);
}
}
function mapStateToProps(state){
return {
categories: state.reducerNotesAndLogin.categories,
categoriesLength: state.reducerNotesAndLogin.categories.length
};
}
export default connect(mapStateToProps)(CategoriesBody);
CategoriesItem.js:
import React from 'react';
import store from './../../../redux/store-index';
import Button from './../../Button';
class CategoryItem extends React.Component {
constructor(props){
super();
this.state = {
edit: false,
categoryName: props.category.categoryName,
categoryColor: props.category.categoryColor
}
this.onClickEdit = this.onClickEdit.bind(this);
this.onChangeCategoryColor = this.onChangeCategoryColor.bind(this);
this.onChangeInputCategoryName = this.onChangeInputCategoryName.bind(this);
this.onClickEditSave = this.onClickEditSave.bind(this);
this.onClickEditCancel = this.onClickEditCancel.bind(this);
}
removeCategory(id, name){
console.log("nsvbsvbfjvbdjhbvv");
store.dispatch({ type: "CATEGORY_REMOVE", payload: {
categoryId: id
}});
// store.dispatch({type: "NOTIFY", payload: {
// type: 'success',
// message: 'Category "' + name + '" removed!'
// }});
}
onClickEdit(){
this.setState({
edit: true
});
}
onChangeCategoryColor(e){
this.setState({
categoryColor: e.target.value
});
}
onChangeInputCategoryName(e){
this.setState({
categoryName: e.target.value
});
}
onClickEditSave(){
this.setState({
edit: false,
categoryName: this.state.categoryName,
categoryColor: this.state.categoryColor
});
store.dispatch({type: "CATEGORY_EDIT", payload: {
categoryId: this.props.category.id,
categoryName: this.state.categoryName,
categoryColor: this.state.categoryColor
}});
store.dispatch({type: "NOTIFY", payload: {
type: "success",
message: "Category saved!"
}});
}
onClickEditCancel(){
this.setState({
edit: false,
categoryName: this.props.category.categoryName,
categoryColor: this.props.category.categoryColor
});
}
render(){
return (
<li key={this.props.category.id} className={this.state.edit === true ? "mt mb" : "flex-justify-between flex-align-center"}>
<div className={this.state.edit === true ? "d-none" : ""}>
<div className="input--color" style={{
backgroundColor: this.state.categoryColor
}}> </div>
{this.state.categoryName}
</div>
{/* Mobile */}
<div className={this.state.edit === true ? "d-none" : "hidden-sm hidden-md hidden-lg"}>
<Button onClick={() => this.onClickEdit()} buttonType="primary">Edit</Button>
<div className="mt-small"> </div>
<Button onClick={() => this.removeCategory(this.props.category.id, this.props.category.categoryName)} type="primary">Remove</Button>
</div>
{/* Tablet and desktop */}
<div className={this.state.edit === true ? "d-none" : "hidden-xs"}>
<div style={{float:'left',}}><Button onClick={() => this.onClickEdit()} buttonType="primary">Edit</Button></div>
<div style={{float:'left',marginLeft:'15px'}}><Button onClick={() => this.removeCategory(this.props.category.id, this.props.category.categoryName)} type="primary">Remove</Button></div>
</div>
{/* EDITING STATE */}
<div className={this.state.edit === true ? "" : "d-none"}>
<div className="row">
<div className="col-xs-12">
<input onChange={this.onChangeCategoryColor} className="input--wide" type="color" value={this.state.categoryColor}
style={{backgroundColor: this.state.categoryColor, height: '30px'}}
/>
<input onChange={this.onChangeInputCategoryName} className="input--wide" type="text" value={this.state.categoryName} />
</div>
</div>
<div className="row mt">
<div className="col-xs-12">
<Button buttonType="primary" onClick={() => this.onClickEditSave()}>Save</Button>
</div>
</div>
<div className="row mt-small">
<div className="col-xs-12">
<Button buttonType="secondary" onClick={() => this.onClickEditCancel()}>Cancel</Button>
</div>
</div>
</div>
</li>
)
}
}
export default CategoryItem;
I think it has something to do with the rendering. Because the arrays are correct when I console.log them. Only the view is different...
Do not modify the state in reducer directly. Create a copy of state value and then modify it.
Change:
var newCategories = state.categories;
To:
var newCategories = [...state.categories];
You should not modify the same array while looping through it.
for (var i = 0; i < newCategories.length; i++) {
if (newCategories[i].id === action.payload.categoryId) {
newCategories.splice(i, 1);
index = i;
i--;
}
}
I got the answer after looking through it with a friend of mine. The solution is pretty simple...
Lesson 101: Make sure that you have a unique "key" property when looping through an array in your UI.
The solution is to add this to my code:
<div key={category.id}>
{this.props.categories.map....
...
</div>
I am still new in React and Redux. So, I know the existence of redux-form, but Im not intend to use in this project. So, what I am doing is creating a form without using redux-form. This form will grab the data from the reducers and pass it to backend API.
This is my main CreateListing.jsx page.
// #flow
import React from 'react';
import { connect } from 'react-redux';
import { Helmet } from 'react-helmet';
import { SEOService } from '[services]';
import CreateListingFormPage1 from './CreateListing/CreateListingFormPage1';
import CreateListingFormPage2 from './CreateListing/CreateListingFormPage2';
import CreateListingFormPage3 from './CreateListing/CreateListingFormPage3';
import WhereAmI from './CreateListing/WhereAmI';
import SuccessCreateListing from './CreateListing/SuccessCreateListing';
type Props = {
...props...
};
class CreateListing extends React.Component<Props> {
getPageBySequence(pagenum) {
// depending on whether User is logged in or not, show/hide the Login/Signup form which is Page3
let sequence = [ CreateListingFormPage1, CreateListingFormPage2, CreateListingFormPage3 ];
if (this.props.isLoggedIn) {
sequence = [ CreateListingFormPage1, CreateListingFormPage2, CreateListingFormPage2 ];
}
return sequence[pagenum-1];
}
getSubmitCreateListing = (e) => {
e.preventDefault();
const propertyType = this.props.listingType;
const propertyName = this.props.suggestedBuildings.selected;
const propertyBuildingType = this.props.propertyBuildingType;
const bedrooms = this.props.bed;
const bathrooms = this.props.bath;
const price = this.props.price;
const builtUp = this.props.builtUp;
const title = this.props.title;
const tenure = this.props.tenure;
const description = this.props.description;
/* IN CASE USER NOT YET LOGGGED IN */
if(this.props.isLoggedIn === false) {
const email = this.props.email;
const password = this.props.password;
this.props.cacheCreateListing({ email, password, propertyType, propertyName, propertyBuildingType, bedrooms, bathrooms, price, builtUp, title, tenure, description });
}
this.props.cacheCreateListing({ propertyType, propertyName, propertyBuildingType, bedrooms, bathrooms, price, builtUp, title, tenure, description });
if(CreateListingFormPage1.landedTypes.includes(propertyBuildingType)) {
this.props.geocodingRequired(true);
}
else {
this.props.geocodingRequired(false);
}
this.props.onSubmitCreateListing();
}
onAuthenticateAndCreateListingButton() {
if(this.props.isLoggedIn) {
return(
<div role="presentation">
<div className={`column ${this.props.formCurrentPage === 1 ? '':'displayNone'}`}>
<button type="button" className="Button button-next is-red" onClick={this.props.onNextClick}>
NEXT
</button>
</div>
<div className={`column ${this.props.formCurrentPage === 2 || this.props.formCurrentPage === 3 ? '':'displayNone'}`}>
<button type="submit" className="Button button-create is-red" onClick={this.props.onLoadingCreateListing}>
CREATE LISTING
</button>
</div>
</div>
)
}
return <div className={`column ${this.props.formCurrentPage < 3 ? '':'displayNone'}`}>
<button type="button" className="Button button-next is-red" onClick={this.props.onNextClick}>
NEXT
</button>
</div>
}
render() {
if(this.props.isListingCreated){
return <SuccessCreateListing />;
}
else if(this.props.isListingLoading){
return <div className="create-listing-spinner" />
}
const CurrentPage = this.getPageBySequence(this.props.formCurrentPage);
return (
<div className={`CreateListing${this.props.isMobile ? '' : ' is-desktop'}`}>
<Helmet>
<title>{ SEOService.getMetaTitle('Create Property Listing') }</title>
{ SEOService.getCanonicalTag('/blogs') }
</Helmet>
<section className="CreateListing--Main">
<div className="CreateListing--Container">
<div className="CreateListing--WhereAmI">
<WhereAmI page={this.props.formCurrentPage} />
</div>
<div className="CreateListing--Body">
<form className="CreateListing--Form" onSubmit={ this.getSubmitCreateListing }>
<CurrentPage />
<div className='columns'>
<div className='column'/>
{/* CHANGE THIS this.props.formCurrentPage < 3 later */}
<div className={`column ${this.props.formCurrentPage > 1 && this.props.formCurrentPage < 4 ? '':'displayNone'}`}>
<button type="button" className="Button button-back" onClick={this.props.onPrevClick}>
BACK
</button>
</div>
{ this.onAuthenticateAndCreateListingButton() }
<div className='column'/>
</div>
</form>
</div>
</div>
</section>
</div>
);
}
};
const MapStateToProps = (state: State) => ({...});
const MapDispatchToProps = (dispatch: Dispatch) => ({
onLoadingCreateListing: () => dispatch({type: 'CREATE_LISTING_LOADING'}),
onSubmitCreateListing: () => dispatch({type: 'CREATE_LISTING_SUBMIT_FORM'}),})
export default connect(MapStateToProps,MapDispatchToProps)(CreateListing);
So, my <input type="text" /> are all from CreateListingFormPage1, CreateListingFormPage2 and CreateListingFormPage3 and put together in <CurrentPage />. My <form onSubmit={...}></form> is in this CreateListing.jsx page. Im not whether it is allowed to do it like this.
So, when I click submit, I got warning of Form submission canceled because the form is not connected.
My example of <input type="" /> in CreateListingFormPage1 are:
// #flow
import React from 'react';
import { connect } from 'react-redux';
import {Dropdown} from '[elements]';
type Props = {...props...};
class CreateListingFormPage2 extends React.Component<Props> {
static get selectTenure() { return ["Select Tenure"].concat(this.tenureTypes) };
static get selectTitle() { return ["Select Title"].concat(this.titleTypes) };
static get selectBedroom() { return["Select Bedrooms no"].concat(this.bedroomNo) };
static get selectBathroom() { return["Select Bathrooms no"].concat(this.bathroomNo) };
static get tenureTypes(){
return[
"FREEHOLD",
"LEASEHOLD",
"OTHERS"
]};
static get titleTypes(){
return[
"RESIDENTIAL",
"COMMERCIAL",
"INDUSTRIAL"
]};
static get bedroomNo(){
return[
"1",
"2",
"3",
"4",
"5"
]};
static get bathroomNo(){
return[
"1",
"2",
"3",
"4",
"5"
]};
get selectTenure() { return this.constructor.selectTenure; }
get selectTitle() { return this.constructor.selectTitle; }
get selectBedroom() { return this.constructor.selectBedroom; }
get selectBathroom() { return this.constructor.selectBathroom; }
get tenureTypes() { return this.constructor.tenureTypes; }
get titleTypes() { return this.constructor.titleTypes; }
get bedroomNo() { return this.constructor.bedroomNo; }
get bathroomNo() { return this.constructor.bathroomNo; }
hasInputError = (name) => {
if (this.props.errors[name]) {
return ' is-error';
}
return '';
}
render() {
return (
<div className={`Listing--Create${ this.props.isMobile ? '' : ' is-desktop' }`} id='form-second-page'>
{/* <form className="Listing--form"> */}
<div className="Listing--bedrooms-bathrooms">
<div className="type-title">No. of Bedrooms</div>
<Dropdown namespace="bedroom" selected={ this.selectBedroom[0] } options={ this.selectBedroom } onOptionSelect={ this.onBedroomDropdownSelect }/>
<div className="type-title">Asking Price</div>
<input className={`text-input price-input${ this.hasInputError('price')}`} type="text" onChange={ (e) => this.props.onPrice(e.currentTarget.value) } value={this.props.price} placeholder="RM"/>
</div>
<div className="Listing--price-built-up">
<div className="type-title">No. of Bathrooms</div>
<Dropdown namespace="bathroom" selected={ this.selectBathroom[0] } options={ this.selectBathroom } onOptionSelect={ this.onBathroomDropdownSelect }/>
<div className="type-title">Built-up Size</div>
<input className={`text-input built-up-input${ this.hasInputError('built_up_size')}`} type="text" onChange={ (e) => this.props.onBuiltUpSize(e.currentTarget.value) } value={this.props.builtUp} placeholder="sqft."/>
</div>
<div className="Listing--tenure">
<div className="type-tenure">Select Tenure</div>
<Dropdown namespace="tenure" selected={ this.selectTenure[0] } options={ this.selectTenure } onOptionSelect={ this.onTenureDropdownSelect }/>
</div>
<div className="Listing--title">
<div className="type-title">Select Title</div>
<Dropdown namespace="title" selected={ this.selectTitle[0] } options={ this.selectTitle } onOptionSelect={ this.onTitleDropdownSelect }/>
</div>
<div className="Listing--photos">
<div className="type-title">Upload Photos</div>
<button className={`text-input photos-input${ this.hasInputError('photos')}`}>Click to upload</button>
</div>
<div className="Listing--description">
<div className="type-title">Describe your property</div>
<textarea className={`text-input description-input${ this.hasInputError('description')}`} onChange={ (e) => this.props.onDescription(e.currentTarget.value) } value={this.props.description} placeholder="Describe your property"/>
</div>
</div>
);
}
};
const MapStateToProps = (state: State) => ({
...
});
const MapDispatchToProps = (dispatch: Dispatch) => ({
...
})
export default connect(MapStateToProps, MapDispatchToProps)(CreateListingFormPage2);
Basically, there is nothing wrong with my redux store. All the value of the input is captured successfully. The problem is when submitting the form, either the onSubmit or my form structure method is incorrect.
This is CreateListing.js reducer should it be helpful:
const INITIAL_STATE= {
isListingLoading: false,
isListingCreated: false,
}
const CreateListing = (state = INITIAL_STATE, action) => {
switch(action.type){
case 'CREATE_LISTING_LOADING':
return Object.assign({}, state, {isListingLoading: true});
case 'CREATE_LISTING_SUBMIT_FORM':
return Object.assign({}, state, {isListingCreated: true});
default:
return state;
} }
export default CreateListing;
Any help is much appreciated.
if you have any other buttons in your form you should add type="button".
so make changes like this.
<button type="button" className="Button button-create is-red" onClick={this.props.onLoadingCreateListing}>
CREATE LISTING
</button>