How to prevent Link from messing up my radio buttons behavior? - reactjs

I am trying to build a menu, that shows the current page in bold.
For that I am using radio buttons so that each item on the menu is a label for that radio button, and in my css I make an active item bold.
My problem is, that because the label is wrapped in a Link element, when an item is clicked nothing really changes. It navigates properly but the radio button sate doesn't change. Maybe everything just re renders ignoring my action?
It works just fine without the link element. Why is that? And what can I do to make it work?
This is the code for my menu component:
import "./styles.scss";
import React from "react";
import { Link } from "react-router-dom";
const Menu = () => {
const turnToLowerCaseWithHyphen = string => {
return (string[0].toLowerCase() + string.slice(1)).replace(" ", "-");
};
const renderMenuItems = array => {
return array.map(item => {
const smallHyphenedItem = turnToLowerCaseWithHyphen(item);
return (
<div className="flex-group" key={smallHyphenedItem}>
<input
className="menu-item__radio"
id={smallHyphenedItem}
type="radio"
name="menu-items"
onChange={() => console.log(smallHyphenedItem)}
/>
<Link
to={"/" + smallHyphenedItem}
className="menu-item"
key={smallHyphenedItem}
>
<label htmlFor={smallHyphenedItem} className="menu-item__label">
{item}
</label>
</Link>
</div>
);
});
};
return (
<div className="menu">
{renderMenuItems(["Feed", "Search", "Contact us"])}
</div>
);
};
export default Menu;
EDIT: I've tried to use a state in the menu component but that doens't help either:
const Menu = () => {
const [currentPage, setCurrentPage] = useState(null);
const turnToLowerCaseWithHyphen = string => {
return (string[0].toLowerCase() + string.slice(1)).replace(" ", "-");
};
const renderMenuItems = array => {
return array.map(item => {
const smallHyphenedItem = turnToLowerCaseWithHyphen(item);
return (
<div className="flex-group" key={smallHyphenedItem}>
<input
className="menu-item__radio"
id={smallHyphenedItem}
type="radio"
name="menu-items"
checked={currentPage === smallHyphenedItem}
onChange={() => setCurrentPage(smallHyphenedItem)}
/>
<label htmlFor={smallHyphenedItem} className="menu-item__label">
<Link
to={"/" + smallHyphenedItem}
className="menu-item"
key={smallHyphenedItem}
>
{item}
</Link>
</label>
</div>
);
});
};
return (
<div className="menu">
{renderMenuItems(["Feed", "Search", "Contact us"])}
</div>
);
};
export default Menu;

This was a bit more of a headach than I've expected, but I got it working.
Like Sasha suggested, I needed to store the choice in a state using redux to have it persist between pages.
But that isn't enough. using Link didn't allow for the action to be executed before navigating (to my understanding).
What I had to do was instead of using Link, to just navigate to the page I wanted using the history.push() command.
This is my final working code:
import "./styles.scss";
import React from "react";
import { connect } from "react-redux";
import { Link, useHistory } from "react-router-dom";
import history from "../../../history";
import { setCurrentPage } from "../../../actions";
const Menu = ({ setCurrentPage, page }) => {
const myHistory = useHistory(history);
console.log(page);
const turnToLowerCaseWithHyphen = string => {
return (string[0].toLowerCase() + string.slice(1)).replace(" ", "-");
};
const handleChange = (page) => {
setCurrentPage(page);
myHistory.push(`/${page}`)
};
const renderMenuItems = array => {
return array.map(item => {
const smallHyphenedItem = turnToLowerCaseWithHyphen(item);
return (
<div className="flex-group" key={smallHyphenedItem}>
<input
className="menu-item__radio"
id={smallHyphenedItem}
type="radio"
name="menu-items"
checked={page === smallHyphenedItem}
onChange={() => handleChange(smallHyphenedItem)}
/>
<label htmlFor={smallHyphenedItem} className="menu-item__label">
{/* <Link
to={"/" + smallHyphenedItem}
className="menu-item"
key={smallHyphenedItem}
> */}
{item}
{/* </Link> */}
</label>
</div>
);
});
};
return (
<div className="menu">
{renderMenuItems(["Feed", "Search", "Contact us"])}
</div>
);
};
const mapStateToProps = state => {
return {
page: state.page
};
};
export default connect(mapStateToProps, { setCurrentPage })(Menu);
And this is my CSS:
.menu {
display: grid;
grid-template-columns: repeat(4, max-content);
grid-gap: 3rem;
&-item {
&__label {
font-family: var(--font-main);
font-size: 1.6rem;
transition: all 0.2s;
text-decoration: none;
color: var(--color-grey-medium);
width: max-content;
&:hover {
color: var(--color-main);
}
}
&__radio {
visibility: hidden;
opacity: 0;
&:checked + .menu-item__label {
color: var(--color-main);
}
}
}
}

Related

React gallery App. I want Add tags to an image individually but the tag is being added to all images. How can I solve this?

**> This is my Gallery Component **
import React, {useState} from 'react';
import useFirestore from '../hooks/useFirestore';
import { motion } from 'framer-motion';
const Gallery = ({ setSelectedImg }) => {
const { docs } = useFirestore('images');
here im setting the state as a Tags array
const [tags, setTags] = useState([""]);
const addTag = (e) => {
if (e.key === "Enter") {
if (e.target.value.length > 0) {
setTags([...tags, e.target.value]);
e.target.value = "";
}
}
};
functions for adding and removing Tags
const removeTag = (removedTag) => {
const newTags = tags.filter((tag) => tag !== removedTag);
setTags(newTags);
};
return (
<>
<div className="img-grid">
{docs && docs.map(doc => (
< motion.div className="img-wrap" key={doc.id}
layout
whileHover={{ opacity: 1 }}s
onClick={() => setSelectedImg(doc.url)}
>
here Im adding the Tag input to each Image...the problem is that when adding a Tag is added to all the pictures. I want to add the tags for the image that I´m selecting.
<div className="tag-container">
{tags.map((tag, ) => {
return (
<div key={doc.id} className="tag">
{tag} <span onClick={() => removeTag(tag)}>x</span>
</div>
);
})}
<input onKeyDown={addTag} />
</div>
<motion.img src={doc.url} alt="uploaded pic"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
>
</motion.img>
</motion.div>
))}
</div>
</>
)
}
export default Gallery;
The tags array that you are using to store values entered by the user are not unique with respect to each image item. Meaning, every image item in your program is using the same instance of the tags array, what you need to do is
Either create an object that stores an array of tags for each image:
const [tagsObj, setTagsObj] = {}, then while adding a new tag for say image_1, you can simply do setTagsObj(prevObj => {...prevObj, image_1: [...prevObj?.image_1, newTagValue]},
Or create an Image Component which would then handle tags for a single image:
Gallery Component:
{
imageList.map(imageEl =>
<ImageItem key={imageEl} image={imageEl} />
)
}
ImageItem Component:
import {useState} from 'react';
export default function ImageItem({image}) {
const [tags, setTags] = useState([]);
const addTag = (e) => {
if (e.key === "Enter") {
const newVal = e.target.value;
if (newVal.length > 0) {
setTags(prevTags => [...prevTags, newVal]);
e.target.value = '';
}
}
};
const removeTag = (removedTag) => {
setTags(prevTags => prevTags.filter((tag) => tag !== removedTag));
}
return (
<div style={{margin: '12px', padding: '12px', width: '100px', height:'100px', display:'flex', flexDirection: 'column', alignItems:'center'}}>
<span>{image}</span>
{tags.map((tag, index) => {
return (
<div key={tag+index}>
{tag} <span onClick={() => removeTag(tag)}>x</span>
</div>
);
})}
<input onKeyDown={addTag} />
</div>
);
}
Refer this sandbox for ease, if available Gallery unique image tags sandbox
I suggest using the second method, as it is easy to understand and debug later on.
I hope this helps, please accept the answer if it does!

TypeError: Cannot read property '0' of undefined when building my react app

while building my react app for deployment, I am getting this error
TypeError: Cannot read property '0' of undefined
when I am rending on port3000 I did not see this error but only get it while building the app.
Can anyone assist to resolve this?
import { useState } from "react";
import styles from "./Tabs.module.css"
const Tabs = ({ children}) => {
const [activeTab, setActiveTab] = useState (children [0].props.label);
const handleClick =( e, newActiveTab ) => {
e.preventDefault();
setActiveTab(newActiveTab);
}
return (
<div>
<ul className= {styles.tabs}>
{children.map ((tab) => {
const label = tab.props.label;
return (
<li
className= {label == activeTab ? styles.current : ""}
key= {label}
>
<a href="#" onClick={(e) => handleClick (e, label)}>{label}
</a>
</li>
)
})}
</ul>
{children.map ((tabcontent1) => {
if (tabcontent1.props.label == activeTab)
return (
<div key= {tabcontent1.props.label} className= {styles.content}>{tabcontent1.props.children}
</div>
);
})}
</div>
);
}
export default Tabs ;
In next js, when you don't put export const getServerSideProps = () => {} in your page then that page is automatically subjected to static side rendering. On development mode, you may see a lightening symbol on bottom-right. Anyway you can read the docs on data-fetching on nextjs. However, your issue on this situation can be easily fixed by setting the children through useEffect.
// handle null on your active tab render function
const [activeTab, setActiveTab] = useState(null);
useEffect(() => {
if(children.length)
children[0].props.label
}, [children])
Another Code Sample:
*A simple change in code structure and the way you are trying to do. It's on react but kind of same in next as well *
import React from "react";
const Tabs = ({ tabsData }) => {
const [activeTabIndex, setActiveTabIndex] = React.useState(0);
const switchTabs = (index) => setActiveTabIndex(index);
return (
<div style={{ display: "flex", gap: 20, cursor: "pointer" }}>
{/* Here active tab is given a green color and non actives grey */}
{tabsData.map((x, i) => (
<div
key={i}
style={{ color: activeTabIndex === i ? "green" : "#bbb" }}
onClick={() => switchTabs(i)}
>
{x.label}
</div>
))}
{/* Show Active Tab Content */}
{tabsData[activeTabIndex].content}
</div>
);
};
export default function App() {
// You can place it inside tabs also in this case
// but lets say you have some states on this component
const tabsData = React.useMemo(() => {
return [
// content can be any component or React Element
{ label: "Profile", content: <p>Verify all Input</p> },
{ label: "Settings", content: <p>Settings Input</p> },
{ label: "Info", content: <p>INput info</p> }
];
}, []);
return (
<div className="App">
<Tabs tabsData={tabsData} />
</div>
);
}
and here is also a example sandbox https://codesandbox.io/s/serverless-night-ufqr5?file=/src/App.js:0-1219

Content disappears after page refresh

I have a problem with my react app. I have a blog page where I can create blog posts and display them to the screen. In this part everything works fine. User logs in and can write a post. Each post contains a Read more... link and if the user clicks on that link the app redirects to the actual blog post. There the user can read the whole blog and add some comments. Everything works perfectly except when the user refreshes the page, everything disappears without any error in the console. I use firebase as my back-end and everything is saved there just like it has to be. Each time I click on the particular post I get redirected to that post and everything is ok, but when I refresh the page everything disappears, the post, the comments, even the input field and the submit comment button.
Here is a picture before refresh:
Before
here is a picture after refresh:
After
Also I will include the code for the actual blog and comment section.
The BlogAndCommentPage contains the actual blog post and holds the input field for the comments and the comments that belong to this post.
import React from 'react'
import { projectFirestore } from '../../firebase/config';
import BackToBlogs from './BackToBlogs'
import AddComment from '../commentComponents/AddComment'
class BlogAndCommentPage extends React.Component {
state = { param: '', blog: [] }
componentDidMount = () => {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString)
const id = urlParams.get('id')
this.setState({ param: id })
const fetchDataFromFireBase = async () => {
projectFirestore.collection('Blogs').doc(id).get()
.then(doc => {
if(doc.exists) {
let document = [];
document.push(doc.data());
this.setState({ blog: document })
}
})
}
fetchDataFromFireBase()
}
renderContent() {
// Display the blog
const blogs = this.state.blog?.map(value => {
return (
<div key={value.post.blogID}>
<h1>{value.post.title}</h1>
<h6>{`${value.post.name} - ${value.post.date}`}</h6>
<p>{value.post.body}</p>
</div>
)
})
return blogs;
}
render() {
const displayedBlog = this.state.param
return (
<div>
{
displayedBlog ? (
<div>
{this.renderContent()}
<BackToBlogs />
<hr></hr>
<h5 className="mb-2">Add a comment</h5>
<AddComment param={this.state.param} />
</div>
) : ''
}
</div>
)
}
}
export default BlogAndCommentPage
The AddComment component holds the submit button for the comments and the list of the components
import React, { useState, useEffect } from 'react'
import SubmitComment from './SubmitComment'
import CommentHolder from './CommentHolder';
import { useSelector, useDispatch } from 'react-redux';
const AddComment = ({ param }) => {
const [comment, setComment] = useState('');
useEffect(() => {
if (sessionStorage.getItem('user') === null) {
alert('You are not logged in. Click OK to log in.')
window.location = 'http://localhost:3000/'
}
}, [])
const dispatch = useDispatch();
const state = useSelector((state) => state.state);
if (state) {
setTimeout(() => {
setComment('')
dispatch({ type: "SET_FALSE" })
}, 50)
}
return (
<div>
<div>
<div className="row">
<div className="col-sm">
<div className="form-group">
<textarea rows="4" cols="50" placeholder="Comment" className="form-control mb-3" value={comment} onChange={(e) => setComment(e.target.value)} />
</div>
</div>
</div>
</div>
<div className="mb-3">
<SubmitComment comment={comment} param={param} />
</div>
<CommentHolder param={param} />
</div>
)
}
export default AddComment
The CommentHolder renders each comment that belong to that post
import React from 'react';
import { projectFirestore } from '../../firebase/config';
import DeleteComment from './DeleteComment'
class CommentHolder extends React.Component {
state = { docs: [] }
_isMounted = false;
componentDidMount = () => {
const fetchDataFromFireBase = async () => {
const getData = await projectFirestore.collection("Comments")
getData.onSnapshot((querySnapshot) => {
var documents = [];
querySnapshot.forEach((doc) => {
documents.push({ ...doc.data(), id: doc.id });
});
if (this._isMounted) {
this.setState({ docs: documents })
}
});
}
fetchDataFromFireBase()
this._isMounted = true;
}
componentWillUnmount = () => {
this._isMounted = false;
}
renderContent() {
// Delete comments
const deleteComment = async (id) => {
projectFirestore.collection('Comments').doc(String(id)).delete().then(() => {
console.log(`Blog with id: ${id} has been successfully deleted!`)
})
}
// Build comments
let user;
if (sessionStorage.getItem('user') === null) {
user = [];
} else {
user = JSON.parse(sessionStorage.getItem('user'));
const commentArray = this.state.docs?.filter(value => value.blogID === this.props.param)
.sort((a, b) => (a.time > b.time) ? -1 : (b.time > a.time) ? 1 : 0)
.map(comment => {
return (
<div key={comment.id} className="card mb-3" >
<div className="card-body">
<div className="row">
<div className="col-sm">
<h6>{`${comment.name} - ${comment.time}`}</h6>
<p>{comment.comment}</p>
</div>
<div className="col-sm text-right">
{user[0].id === comment.userID ? <DeleteComment commentid={comment.id} onDeleteComment={deleteComment} /> : ''}
</div>
</div>
</div>
</div>
)
});
const updateComments = () => {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString)
const id = urlParams.get('id')
const updateComment = projectFirestore.collection('Blogs').doc(id);
return updateComment.update({
'post.comments': commentArray.length
})
}
updateComments()
return commentArray;
}
}
render() {
return (
<div>
{this.renderContent()}
</div>
)
}
}
export default CommentHolder
The DeleteComment deletes the comment
import React from 'react'
const DeleteComment = ({ commentid, onDeleteComment }) => {
return (
<div>
<button onClick={() => onDeleteComment(commentid)} className='btn btn-outline-danger'>X</button>
</div>
)
}
export default DeleteComment
The SubmitComment stores the comment in the Firebase
import React from 'react'
import { projectFirestore } from '../../firebase/config';
import { v4 as uuidv4 } from 'uuid';
import { useDispatch } from 'react-redux';
const SubmitComment = ({ comment, param }) => {
const dispatch = useDispatch();
const onCommentSubmit = () => {
let user;
if (sessionStorage.getItem('user') === null) {
user = [];
} else {
user = JSON.parse(sessionStorage.getItem('user'));
projectFirestore.collection('Comments').doc().set({
id: uuidv4(),
comment,
name: `${user[0].firstName} ${user[0].lastName}`,
userID: user[0].id,
blogID: param,
time: new Date().toLocaleString()
})
dispatch({ type: "SET_TRUE" });
}
}
return (
<div>
<button onClick={() => onCommentSubmit()} className='btn btn-primary'>Add comment</button>
</div>
)
}
export default SubmitComment
In case there is a rout problem here is the code for the routing between the blogs section and the blog + comments section
return (
<Router >
<Route path='/content-page' exact render={(props) => (
<>
<BlogAndCommentPage />
</>
)} />
<Route path='/blogpage' exact render={(props) => (
<>
<div>
<div className="row">
<div className="col-8">
<h1 className='mb-3'>Blog</h1>
</div>
<div className="col-4 mb-3">
<LogoutButton onLogOut={logout} />
<h6 className='float-right mt-4 mr-2'>{displayUser}</h6>
</div>
</div>
{empty ? (<div style={{ color: "red", backgroundColor: "#F39189", borderColor: "red", borderStyle: "solid", borderRadius: "5px", textAlign: 'center' }} className="mb-2">Title and body cannot be blank</div>
) : ("")}
<InputArea getBlogContent={getBlogContent} />
<CreateBlog post={post} onCheckEmptyInputs={checkEmptyTitleAndBody} />
<hr />
<BlogHolder />
</div>
</>
)} />
</Router>
)
If anybody has any clue on why is this happening, please let me know.
Thank you.
As your website is CSR (client side rendering) it doesn't understand the URL in the first execution, you might need to configure a hash router, take a look at:
https://reactrouter.com/web/api/HashRouter
Also, there is a good answer about it here

How do I return an input tag from a function?

I have a button on my webpage, and I want an input tag to appear, whenever the user clicks that button. I earlier tried something like this:
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [showInput, setShowInput] = useState(false);
const handleClick = () => setShowInput(true);
return (
<div className="App">
<button onClick={handleClick}>Click me</button>
{showInput ? <input type="text" /> : ""}
</div>
);
}
But this only worked once. I want it to add an input tag whenever the user clicks that button. How do I do so?
Instead of maintaining the number of input elements in the state, i suggest that you maintain an object in the state that is initially empty. Once the button is clicked to add an input, you could update the object with a key-value pair that represents the new input element.
State after adding one input could like as shown below:
{
input1: { value: '' }
}
Similarly, as more inputs are added, more objects will be added in the state.
This will allow your input elements to be controlled components and will allow you to handle the onChange event with only one event handler function.
Demo
let counter = 1;
function App() {
const [inputs, setInputs] = React.useState({});
const handleClick = () => {
const inputName = "input" + counter++;
const inputObj = { value: "" };
setInputs({ ...inputs, [inputName]: inputObj });
};
const handleChange = (event) => {
const { name, value } = event.target;
setInputs({ ...inputs, [name]: { ...inputs[name], value } });
};
return (
<div className="App">
<button onClick={handleClick}>Add Input</button>
<div className="inputContainer">
{Object.keys(inputs).map((inputName) => {
const { value } = inputs[inputName];
return (
<input
key={inputName}
name={inputName}
value={value}
onChange={handleChange}
placeholder={inputName}
/>
);
})}
</div>
</div>
);
}
ReactDOM.render(<App/>, document.querySelector('#root'));
.App {
font-family: sans-serif;
text-align: center;
}
.inputContainer {
display: flex;
flex-direction: column;
max-width: 300px;
margin: 10px auto;
}
input {
margin: 5px;
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Make showInput a number that defaults to 0.
Have handleClick increment that number instead of just setting true.
Outside the return expression, create an array. With a for loop, push inputs (until you reach the number specified) into the array.
Replace the line where you add the input to the JSX with that array.
Something like ...
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [inputs, setInputs] = useState([]);
const handleClick = () => setInputs([...inputs, ""]);
return (
<div className="App">
<button onClick={handleClick}>Click me</button>
{inputs.map(i => <input type="text"/>)}
</div>
);
}
Now you can also store your input values into your inputs state for further processing.
I leave formatting up to you ... !
import React, { useState } from "react";
export default function App() {
const initialValue = [{ value: "first input" }];
const [userInputs, setUserInputs] = useState(initialValue);
const handleClick = () => {
const updatedInputs = [...userInputs, { value: "new input"}]
setUserInputs(updatedInputs);
}
return (
<div className="App">
<button onClick={handleClick}>Click me</button>
{userInputs.map((el, i) => (
<input type="text" value={el.value} />
))}
</div>
);
}
All of the implementation above is correct, But I also have my own implementation.
import React, { useState, Fragment } from "react";
export default function App() {
const [showInputs, setInputs] = useState([]);
const handleClick = () => {
setInputs((prev) => {
const i = prev.length + 1;
return [
...prev,
<Fragment key={i}>
<input type="text" />
<br />
</Fragment>
];
});
};
return (
<div className="App">
<button onClick={handleClick}>Click me</button>
<br />
{showInputs}
</div>
);
}

Component don't re render after state update

I'm testing out react-sweet-state as an alternative to Redux, but I feel like I'm missing something.
This is how my store is written :
import { createStore, createHook } from 'react-sweet-state'
const initialState = {
startHolidayButton: true,
}
const actions = {
setStartHolidayButton: value => ({ setState }) => {
setState({ startHolidayButton: value })
},
}
const ButtonsVisibleStore = createStore({
initialState,
actions,
name: 'ButtonsVisibleStore',
})
export const useButtonsVisible = createHook(ButtonsVisibleStore)
In my eyes it all seems pretty fine, the hooks works as long as I only need the initial state.
This is how I access and modify it :
const App = () => {
const [state, actions] = useButtonsVisible()
return (
<Styled>
<MainMenu />
<div className="l-opacity">
<WorldMap />
</div>
{state.startHolidayButton && (
<div
className="bottom-toolbar"
onClick={() => {
actions.setStartHolidayButton(false)
}}>
<StartSelectionButton />
</div>
)}
</Styled>
)
}
I can read the values from the state and I can trigger the actions but if an action updates a state value, my component don't re render, it's like he is unaware that something has been updated.
So am I doing it the right way or are actions not meant for this?
export default function App() {
const [state, actions] = useButtonsVisible();
return (
<div className="App">
{state.startHolidayButton ?
(
<div
className="bottom-toolbar"
onClick={() => {
actions.setStartHolidayButton(false);
}}
>
click me
</div>
)
:
(
<div
className="bottom-toolbar green"
onClick={() => {
actions.setStartHolidayButton(true);
}}
>
click me
</div>
)
}
</div>
);
}
With some changes your code works fine. Here is the css: .bottom-toolbar { display: inline-block; width: 100%; height: 50px; background: red; cursor: pointer; } .bottom-toolbar.green { background: green; }
https://codesandbox.io/s/elegant-mendeleev-o9zv7?file=/src/styles.css:58-218

Resources