I am trying to render UI in my project based on selected roles (brands, agency, influencer) on click. However, the logic that I am putting together is not loading the right UI and I don't quiet understand why not.
I have tried moving the role and setRole to the top component and passed the props down to the child components that read role and updated it via setRole so that I can have the state to be available in two places.
I also set a logic that should display components based on if the role equals the value of the buttons.
What happened was the components weren't loading upon clicking the function that handles click. However, logging out to the console if the role equals the value of the clicked button returns true, the right string that the logic was based on.
What I am expecting to happen is to load the component e.g: "Brands" when users click and select "brands" which is the value of the clicked button. Vise versa for the other components.
My code is as follows:
import { useState } from 'react';
import { useSession } from 'next-auth/react';
import Brands from './Brands';
import Agency from './Agency';
import CreatorsDash from './CreatorsDashboard';
export default function FirstPageModal({ role: userRole }) {
const [role, setRole] = useState(userRole);
const { data: session } = useSession();
const handleClick = (e) => {
e.preventDefault();
let buttonValue = e.target.value;
const clickedRole = role?.map((user) => {
let { role } = user;
if (buttonValue) {
userRole = { role: buttonValue };
}
return { userRole };
});
console.log(clickedRole); //Returns an array
clickedRole.map((item) => {
const { role } = item.userRole;
console.log(role); //Returns string ("agency" / "brands" / "Influencer")
if (session && role === 'brands') {
console.log(role); //This logs "brands" as expected but doesn't return the component
// return <Brands session={session} role={role} />;
} else if (session && role === 'agency') {
return <Agency session={session} role={role} />;
} else if (session && role === 'Influencer') {
return <CreatorsDash session={session} role={role} />;
} else {
console.log('Select a role');
}
});
};
return (
<>
<div className="">
<button type="button" className="" onClick={handleClick} value="agency">
As an Agency
</button>
<button type="button" className="" onClick={handleClick} value="brands">
As a Brand
</button>
<button
type="button"
className=""
onClick={handleClick}
value="Influencer"
>
As an Influencer
</button>
</div>
</>
);
}
Returning a component from an onClick handler doesn't automatically render the component. One thing you could do is to keep track of the role in the state and then put the <Brands /> <Agency/> and <CreatorsDash /> components in the render function and dynamically show/hide them like {role === "brands" && <Brands />. This can also be done with css, although the benefits of this are not so clear,.
Side note, it is very helpful to post a codepen with your code, especially as your code gets more complicated
Related
I am trying to create a notes application wherein each note object contains a title and content. The user can add, deleted and update a note.
What I was able to achieve so far:
I am able to create a new note , push it into an array and also delete a note from the array . I am finding it a bit hard to edit an existing note.
This is how I want to implement the edit feature:
When the user clicks on the note, the data has to automatically fill into the input box, and the user can modify the data which is then saved into an object and pushed inside an array and then displayed onto the respective note.
When the user clicks on the Edit button, the note id is sent to the App component, the note is searched within the notes array and an object returned to the Create Area component. This object is then displayed on the input field. I'm using UseEffect() hook to display the object data on the input box, but I'm not able to edit the contents on the input box. Here's my code below:
App.jsx:
If the user clicked the edit button, it sets the IsDone state to true in the Edit function. The edit function gets an object from the Notes component
import Header from "./Header";
import CreateArea from "./CreateArea";
import Note from "./Note";
import Footer from "./Footer";
import { useState } from "react";
function App() {
const [noteArray, setArray] = useState([]);
const [isDone,setDone] = useState(false);
const [editNote,setEditNote] = useState({
title:"",
content:""});
function AddOnClick(note) {
setArray((prevNote) => {
return [...prevNote, note];
});
}
function DeleteOnClick(id) {
setArray((prevNote) => {
return prevNote.filter((note, index) => {
return index !== id;
});
});
}
function EditNote(obj)
{
setDone(true);
setEditNote(prevState=>{
return { ...prevState,...obj}});
}
return (
<div>
<Header />
<CreateArea AddOnClick={AddOnClick} noteEdit = {editNote} editFunction = {EditNote}btnClicked = {isDone}/>
{noteArray.map((note, index) => (
<Note
key={index}
id={index}
title={note.title}
content={note.content}
deleteNote={DeleteOnClick}
EditNote = {EditNote}
/>
))}
<Footer />
</div>
);
}
export default App;
Notes.jsx: The id of the note is also included in the object that's passed to App component through the EditNote() function
function Note(props) {
const obj = {title : props.title,
content: props.content,
id:props.id}
return (
<div className="note">
<h1>{props.title}</h1>
<p>{props.content}</p>
<button
onClick={() => {
props.deleteNote(props.id);
}}
>
DELETE
</button>
<button onClick={()=>{props.EditNote(obj)}}>EDIT</button>
</div>
);
}
export default Note;
CreateArea: If the buttonClicked value is true, I'm calling the handleEdit() that takes the object sent from the EditNote() in App.jsx to saves it to to note object using useState() which automatically updates the input and text area field using event.target.value with the help of useEffect().
import { useState } from "react";
function CreateArea(props) {
const [note, setNote] = useState({
title: "",
content: ""
});
function handleChange(event) {
console.log(event.target);
const { name, value } = event.target;
setNote((prevNote) => {
return { ...prevNote, [name]: value };
});
}
function addNote(event) {
setNote({ title: "", content: "" });
props.AddOnClick(note,note.id);
event.preventDefault();
}
function handleEdit()
{
setNote(prevValue=>{
return {...prevValue,...props.noteEdit}
})
}
useEffect (()=>{
if(props.btnClicked){handleEdit();
}
});
return (
<div>
<form>
<input name="title" id="title" value={note.title}onChange={handleChange}placeholder="Title"/>
<textarea name="content" id="content" value={note.content}onChange={handleChange} placeholder="Take a note..." rows="3"/>
<button onClick={addNote}>Add</button>
</form>
</div>
);
}
export default CreateArea;
The code runs well but now I can't add any more text on the input box,it just blocks me from doing it.I tried calling HandleChange() inside UseEffect(), that throws an error saying: Cannot read properties of target:undefined at HandleChange() I really need help how to implement edit.
I tried directly populating the input box and the text area field using document.getElementById.value = myValue even that does not seem to work
Right now, I have a react portal rendering a 'pinnable' side drawer modal that will display based on a redux state. The contents of the modal will have information based on where that modal was pinned from, in this case my notifications.
The problem I'm running into at the moment is that since the modal will be pinnable in multiple places, I'm not exactly sure of the logic on how to handle the modal contents if the modal is already pinned.
I've tried/considered the following:
Just have one portal render its children dynamically. Unfortunately the location of where the portal will be rendered does not contain the contents and logic of the modal, so I believe this can't be done.
Compare props.children and if they're not identical, render the newer portal and deconstruct the other. I'm hesitant to use this approach since I believe there's a better solution out there.
Render the portals based on Ids and deconstruct/reconstruct where needed if one exists. I'm leaning towards this approach, but again I'd like to see if there's a better one.
Portal location:
export default function PaperContainer() {
return <div id="pinned-container"></div>;
}
Portal:
export default function PinnedContainer(props) {
const pinned = useSelector(state => state.ui.isDrawerPinned);
return (
pinned &&
createPortal(
<div>
<div>{props.children}</div>
</div>
,
document.getElementById('pinned-container')
)
);
}
Where the portals are called (simplified for brevity):
export default function PortalCallLocationOne() {
const dispatch = useDispatch();
const pinContainer = () => {
dispatch(toggleDrawer());
};
return (
<>
<Button startIcon={<Icon>push_pin</Icon>} onClick={() => pinContainer}>
Pin Notification
</Button>
<PinnedContainer>
//Notification
</PinnedContainer>
</>
);
}
export default function PortalCallLocationTwo() {
const dispatch = useDispatch();
const pinContainer = () => {
dispatch(toggleDrawer());
};
return (
<>
<Button startIcon={<Icon>push_pin</Icon>} onClick={() => pinContainer}>
Pin List
</Button>
<PinnedContainer>
// List
</PinnedContainer>
);
</>
}
I tried going off of #3 and destroying pinned-container's first child if it existed and replace it with the new children. This didn't work since React was expecting that child and kept throwing failed to execute removeChild on node errors.
Unfortunately it looks like react is unable to replace portal children instead of appending them.
However, I was able to solve my issue by unpinning the portal and repinning it with redux actions.
export default function PinnedContainer(props) {
const pinned = useSelector(state => state.ui.isDrawerPinned);
useEffect(() => {
if (pinned) {
dispatch(clearPinned());
dispatch(pinDrawer(true));
}
}, []);
return (
pinned &&
createPortal(
<div>
<div>{props.children}</div>
</div>
,
document.getElementById('pinned-container')
)
);
}
Reducer:
export const initialState = {
isDrawerPinned: false,
}
export const reducer = (state = initialState, action) => {
switch(action.type){
case actionTypes.PIN_DRAWER:
return {
...state,
isDrawerPinned: action.isPinned ? action.isPinned : !state.isDrawerPinned,
};
case actionTypes.CLEAR_PINNED:
return {
...state,
isDrawerPinned: state.isDrawerPinned ? !state.isDrawerPinned : state.isDrawerPinned
};
}
}
In my React application I have a component called Value, which has several instances on multiple levels of the DOM tree. Its value can be shown or hidden, and by clicking on it, it shows up or gets hidden (like flipping a card).
I would like to make 2 buttons, "Show all" and "Hide all", which would make all these instances of the Value component to show up or get hidden. I created these buttons in a component (called Cases) which is a parent of each of the instances of the Value component. It has a state called mode, and clicking the buttons sets it to "showAll" or "hideAll". I use React Context to provide this chosen mode to the Value component.
My problem: after I click the "Hide All" button and then make some Value instances visible by clicking on them, I'm not able to hide all of them again. I guess it is because the Value components won't re-render, because even though the setMode("hideAll") function is called, it doesn't actually change the value of the state.
Is there a way I can make the Value instances re-render after calling the setMode function, even though no actual change was made?
I'm relatively new to React and web-development, I'm not sure if it is the right approach, so I'd also be happy to get some advices about what a better solution would be.
Here are the code for my components:
const ModeContext = React.createContext()
export default function Cases() {
const [mode, setMode] = useState("hideAll")
return (
<>
<div>
<button onClick={() => setMode("showAll")}>Show all answers</button>
<button onClick={() => setMode("hideAll")}>Hide all answers</button>
</div>
<ModeContext.Provider value={mode}>
<div>
{cases.map( item => <Case key={item.name} {...item}/> ) }
</div>
</ModeContext.Provider>
</>
)
}
export default function Value(props) {
const mode = useContext(ModeContext)
const [hidden, setHidden] = useState(mode === "showAll" ? false : true)
useEffect(() => {
if (mode === "showAll") setHidden(false)
else if (mode === "hideAll") setHidden(true)
}, [mode])
return (
hidden
? <span className="hiddenValue" onClick={() => setHidden(!hidden)}></span>
: <span className="value" onClick={() => setHidden(!hidden)}>{props.children}</span>
)
}
You first need to create your context before you can use it as a provider or user.
So make sure to add this to the top of the file.
const ModeContext = React.createContext('hideAll')
As it stands, since ModeContext isn't created, mode in your Value component should be undefined and never change.
If your components are on separate files, make sure to also export ModeContext and import it in the other component.
Example
Here's one way to organize everything and keep it simple.
// cases.js
const ModeContext = React.createContext('hideAll')
export default function Cases() {
const [mode, setMode] = useState("hideAll")
return (
<>
<div>
<button onClick={() => setMode("showAll")}>Show all answers</button>
<button onClick={() => setMode("hideAll")}>Hide all answers</button>
</div>
<ModeContext.Provider value={mode}>
<div>
{cases.map( item => <Case key={item.name} {...item}/> ) }
</div>
</ModeContext.Provider>
</>
)
}
export function useModeContext() {
return useContext(ModeContext)
}
// value.js
import { useModeContext } from './cases.js'
export default function Value(props) {
const mode = useContext(ModeContext)
const [hidden, setHidden] = useState(mode === "showAll" ? false : true)
useEffect(() => {
if (mode === "showAll") setHidden(false)
else if (mode === "hideAll") setHidden(true)
}, [mode])
return (
hidden
? <span className="hiddenValue" onClick={() => setHidden(!hidden)}></span>
: <span className="value" onClick={() => setHidden(!hidden)}>{props.children}</span>
)
}
P.S. I've made this mistake many times, too.
You shouldn't use a new state in the Value component. Your components should have an [only single of truth][1], in your case is mode. In your context, you should provide also a function to hide the components, you can call setHidden
Change the Value component like the following:
export default function Value(props) {
const { mode, setHidden } = useContext(ModeContext)
if(mode === "showAll") {
return <span className="hiddenValue" onClick={() => setHidden("hideAll")}></span>
} else if(mode === "hideAll") {
return <span className="value" onClick={() => setHidden("showAll")}>{props.children}</span>
} else {
return null;
}
)
}
P.S. Because mode seems a boolean value, you can switch between true and false.
[1]: https://reactjs.org/docs/lifting-state-up.html
There are a few ways to handle this scenario.
Move the state in the parent component. Track all visible states in the parent component like this:
const [visible, setVisibilty] = useState(cases.map(() => true))
...
<button onClick={() => setVisibilty(casses.map(() => false)}>Hide all answers</button>
...
{cases.map((item, index) => <Case key={item.name} visible={visible[index]} {...item}/> ) }
Reset the mode after it reset all states:
const [mode, setMode] = useState("hideAll")
useEffect(() => {
setMode("")
}, [mode])
I have a dropdown searchbox that I want to use to display results and I have a card component that I want to display when the dropdown value matches the value of the user the card component is for.
I have retrieved the state of the user from props and compared this with the state of the dropdown to create the function onSearchSubmit to display the card component 'ProfileCard'.
When testing the onSearchSubmit function using console.log, the if statement is true however the ProfileCard component does not display. There are no errors, I just can't see my component being displayed.
Any help will be greatly appreciated. This is my first question on here so if there is not enough information I will try and help as much as I can.
Please see code below. (I have omitted sections of the code I don't think is related to this section).
onSearchSubmit = () => {
if (this.state.selectedCategory === this.props.category) {
console.log('it works')
return ( <ProfileCard/> );
} else {
console.log('not working')
}
}
render() {
return (<div>
<button onClick = {
this.onSearchSubmit
}> Search</button>
</div>
)
}
Thanks,
John
You are using conditional rendering wrong way. Put the component in the render and add condition there based on some variable.
onSearchSubmit = () => {
if (this.state.selectedCategory === this.props.category) {
console.log('it works')
this.setState({
renderProfileCard: true
})
} else { console.log('not working') }
}
render() {
return (
<div>
<button onClick={this.onSearchSubmit}>Search</button>
{this.state.renderProfileCard && <ProfileCard />}
</div>
)
}
Add renderProfile flag to your state.
this.state = {
renderProfileCard: false
}
You should return the component inside the render hook itself:
render() {
return (
<div>
<button onClick={this.onSearchSubmit}>Search</button>
{
this.state.selectedCategory === this.props.category &&
<ProfileCard /> || null
}
</div>
)
}
If you want to render from outside, the I would suggest you to look at my another post most efficient way of rendering JSX element.
I have two types of item, one of which can contain data similar to the other.
Currently when form is used to save an item it saves it then uses browserHistory.push to show the next page.
But I wish add a button that will
save the currently item
redirect them to the form to add the other item type,
partially fill out this form with the data from the first item.
Is there a way to do this using react and not using local storage or session variables?
You should take a look to Redux (or other Flux based libraries) to store data between components and routes, avoiding the excessive prop nesting.
browserHistory.push won't work. It only moves you to a certain location but it doesn't update the application state. You need to update application state, which then will reflect into location update, but not in the opposite direction. Keep in mind that, in React, data comes first, and its representation, even though mutable, doesn't change the data back. The same applies to the location.
To make the redirect alone work, I'd recommend wrapping your component into withRouter higher-order component.
import React, { Component } from 'react';
import { withRouter } from 'react-router';
class MyComponent extends Component {
render() {
return (
<div>
<button
onClick={() => this.props.router.push('/new-location')}>
Click me to go to /new-location
</button>
</div>
);
}
}
But if you need to pass data from one component to another, and the two aren't in hierarchy, I'd agree with Alomsimoy and recommend using Redux. But if, for some reason, it's not an option, you can store this data in a component that is parent to both forms:
class FormA extends Component {
render() {
return (
<form onSubmit={() => this.props.onSubmit()}>
<input
type="text"
value={this.props.inputA}
onChange={(event) => this.props.handleChangeA(event)} />
</form>
);
}
}
class FormB extends Component {
render() {
return (
<form onSubmit={() => this.props.onSubmit()}>
<input
type="text"
value={this.props.inputB}
onChange={(event) => this.props.handleChangeB(event)} />
</form>
);
}
}
while their parent would rule the location and state updates:
class Forms extends Component {
constructor() {
super();
this.state = {};
}
handleChange(name, value) {
this.setState({
[name]: value
});
}
renderForm() {
const {
params: {
stepId
}
} = this.props;
if (stepId === 'step-a') { // <- will be returned for location /form/step-a
return (
<FormA
inputA={this.state.inputA}
handleChangeA={(event) => this.handleChange('inputA', event.target.value)}
onSubmit={() => this.props.router.push('/form/step-b')} />
);
} else if (stepId === 'step-b') { // <- will be returned for location /form/step-b
return (
<FormB
inputB={this.state.inputB}
handleChangeB={{(event) => this.handleChange('inputA', event.target.value)} />
);
}
}
render() {
const {
children
} = this.props;
console.log(this.state); // track changes
return (
<div>
{this.renderForm()}
<button
onClick={() => this.props.router.push('/new-location')}>
Click me to go to /new-location
</button>
</div>
);
}
}
export default withRouter(Forms);
so the route for them would look like
<Route path="form/:stepId" component={Forms} />