ReactJS | Map through nested objects - javascript

I have the following object coming from the BE. I load it through redux, onto a property inside my Component called user. This is the schema:
{
user: 'root',
perm: [
"admin",
"write",
"read",
"nodata"
],
"auth": {
"mode1": true,
"mode2": true,
"mode3": false
}
}
What I want to do, is actually, map through the auth field, and for every mode, print in a span the name of each mode(THE KEY OF THE OBJECT).
So, I created a little renderSpan method to create the necessary boilerplate to map through:
renderSpan = (authKey: string): JSX.Element => {
const { user } = this.props;
return (
<div key={authKey}>
{user && user.auth && <span> {user.auth[securityKey]}</span>}
</div>
);
};
Then I mapped through it on my main render block:
renderAuthModes = () => {
const { user } = this.props;
return (
<Fragment>
{user && user.auth && (
<div className="container-fluid">
{JSON.stringify(user.auth, null, 2)} // This prints the 3 modes
<div>
{Object.keys(user.auth).map(authKey => this.renderSpan(authKey))}
</div>
</div>
)}
</Fragment>
);
};
What I get in return, is 3 div with empty spans. The divs are rendered on to the DOM, but nothing is printed on the display. What am I doing wrong?
The Application is in React-Redux and Typescript.

{user.auth.[securityKey]}</span>}
Should be
{user.auth[securityKey]}</span>}
Besides that everything works fine. The problem is that you are just rendering a span tag with a boolean inside, so the content will be empty... Try something like this:
<span> {user.auth[securityKey] ? 'true' :'false'}</span>

Related

how to fix Error: Hydration failed because the initial UI does not match what was rendered on the server in my code like this

export default function Page({ data1 }) {
const [bookmark, setBookmark] = useState(
typeof window !== 'undefined'
? JSON.parse(localStorage.getItem('bookmark'))
: []
);
const addToBookmark = (ayatLs) => {
setBookmark([...bookmark, ayatLs]);
};
useEffect(() => {
localStorage.setItem('bookmark', JSON.stringify(bookmark));
}, [bookmark]);
return (
<>
<div className="modal-body">
<ul>
{bookmark.map((x) => (
<li key={x.nomor}>{x.tr}</li>
))}
</ul>
</div>
</>
);
}
{data1.map((x)) => (
<div className="pb-6 flex justify-between">
<span
onClick={() => addToBookmark(x)}
className={`text-sm `}>
{x.tr}
</span>
</div>
)}
When i use typeof window !== 'undefined' the Error: Hydration failed because the initial UI does not match what was rendered on the server in my code like this.
and when i change to localStorage.getItem('bookmark') the error is localStorage is not defined
when i click addToBookmark it will store data from props data1 to localStorage, no problem here, but when I fetch the data earlier in localStorage and I want to map the data that error appears
What's wrong with my code, why can't I map data from localStorage, please help me solve this problem.
The issue is that Next.js pre-renders pages by default, which means they are rendered on the server first and then sent to the client to be hydrated.
The error comes when you're setting the default value of the state based on this condition: typeof window !== 'undefined'.
Consider the following example:
const Page = () => {
const [name, setName] = useState(typeof window !== 'undefined' ? 'Peter' : 'Rick')
return <h1>{name}</h1>
}
export default Page
This code will throw an error of the type Error: Text content does not match server-rendered HTML.
If you inspect the page's source, you will see that the name rendered on the server was "Rick" while on the client-side's first render the name rendered was "Peter". There must be a match between the server-side rendered content and the client-side first render's content.
What you can do is move the localStorage data gathering and parsing logic to another useEffect instead and set the state in there. This solves the issue because useEffect only runs on the client-side, therefore you will effectively match both server-side and client-side first render content.
export default function Page({ data1 }) {
const [bookmark, setBookmark] = useState([])
const addToBookmark = (ayatLs) => {
setBookmark([...bookmark, ayatLs])
}
useEffect(() => {
if (bookmark.length === 0) return
localStorage.setItem('bookmark', JSON.stringify(bookmark))
}, [bookmark])
useEffect(() => {
const bookmarkFromLocalStorage = localStorage.getItem('bookmark')
const parsedBookmark =
bookmarkFromLocalStorage !== null
? JSON.parse(bookmarkFromLocalStorage)
: []
setBookmark(parsedBookmark)
}, [])
return (
<>
<div className='modal-body'>
<ul>
{bookmark.map((x) => (
<li key={x.nomor}>{x.tr}</li>
))}
</ul>
</div>
</>
)
}

How can I match data from an object and pass it as props to another component that displays dynamically?

I am storing my data in JS array. After that I am using map function to display components with specific data. Until then, everything works.
After I click component I want to display a PopUp component that display more specific data from the data.js file. How can I pass it as props and match correct data to correct component? At this moment after I click it always shows me the same text.
My data.js file:
export const data = {
projects: [
{
name: "Project1",
image: require("../assets/img/Project1.PNG"),
description: "Set number 1",
technologies: "REACT",
link: "github.com",
},
{
name: "Project2",
image: require("../assets/img/Project2.PNG"),
description: "Project number 2 - description",
technologies: "C#",
link: "github.com",
},
This is my PopUp.js that should display more specific data:
function Popup({ project, description }) {
return (
<Wrapper>
<p>Project name is: {project}</p>
<p>Description is: {description}</p>
</Wrapper>
);
}
And here I map my data and here is the problem.
How can I pass it?
function Projects() {
const [isOpen, setIsOpen] = useState(false);
const togglePopup = () => {
setIsOpen(!isOpen);
};
return (
<Wrapper>
{data.projects.map((proj) => (
<ProjectContainer background={proj.image} onClick={togglePopup}>
{isOpen && (
<Popup project={proj.name} description={proj.description} />
)}
</ProjectContainer>
))}
</Wrapper>
);
}
Really thanks for all of the answers!
You can use index instead of boolean :
function Projects() {
const [isOpen, setIsOpen] = useState('nothing');
return (
<Wrapper>
{data.projects.map((proj, index) => (
<ProjectContainer background={proj.image}
onClick={()=>setIsOpen(oldIndex => index === isOpen ? 'nothing' : index)}>
{isOpen === index && (
<Popup project={proj.name} description={proj.description} />
)}
</ProjectContainer>
))}
</Wrapper>
);
}
The state stores the index of the current opened popup.
ProjectContainer onClick event check if the current index is the same as their and changes accordingly : to 'nothing' if they are currently opened, to their index if they are not.
The condition {isOpen === index && ( is used to show only the current opened popup ie the current index.
This way you can toggle an individual project and not all of them.

Skills list is not updated when new skill is added

There is an accordion of Skills and Experiences in background component. When the title on the skills accordion is clicked then the modal will pop up with skill form which when filled and submitted then the modal closes and the list of skill should get update but it is not updating. I have to refresh to see the changes in the skill list. Here is how i have done
class Background extends React.PureComponent {
state = {
show: false,
componentName: null,
activeIndex: 0
};
handleModal = (action, componentName) =>
this.setState(state => ({
show: action,
componentName
}));
render() {
const { show, activeIndex, componentName } = this.state;
return (
<React.Fragment>
<ProfileWrapper>
<Query query={BACKGROUND_QUERY}>
{({
data: { experiences, skills, languages, educations } = {},
loading
}) => {
if (loading) {
return <h1>Loading...</h1>;
} else {
return (
<Grid columns={2}>
<Accordion
index={1}
onToggle={this.handleToggle}
css="max-width: 100%; min-width: 200px;"
>
<Accordion.Title>
<Title>
Skills (
{`${skills !== undefined && skills.edges.length}`})
</Title>
</Accordion.Title>
<Accordion.Content>
<Content>
{skills !== undefined && skills.edges.length > 0 && (
<span>
{skills.edges.map(skill => {
return (
<React.Fragment key={skill["node"].id}>
<span key={skill["node"].id}>
<Chip>{skill["node"].title}</Chip>
</span>
</React.Fragment>
);
})}
</span>
)}
</Content>
</Accordion.Content>
</Accordion>
<Modal
position="centerCenter"
open={show}
onClose={() => this.handleModal(false, null)}
>
<React.Fragment>
{componentName !== null &&
componentName === "experiences" && (
<Experiences experiences={experiences} />
)}
{componentName !== null &&
componentName === "skills" && (
<Skills skills={skills} />
)}
</React.Fragment>
</Modal>
</Grid>
);
}
}}
</Query>
</ProfileWrapper>
</React.Fragment>
);
}
}
export default Background;
const Skills = ({ handleSubmit, ...props }) => {
const formSubmit = async (val, mutation) => {
const {
data: { skill: response }
} = await mutation({
variables: val
});
console.log('response', response);
if (response.success) {
props.closeModal();
toast.success("New Skill Added!");
}
};
return (
<React.Fragment>
<Mutation mutation={CREATE_SKILL}>
{mutation => {
return (
<form onSubmit={handleSubmit(val => formSubmit(val, mutation))}>
<Field name="title" label="Title" component={TextField} />
<Button>
<Button.Primary>Add Skill</Button.Primary>
<Button.Secondary>Cancel</Button.Secondary>
</Button>
</form>
);
}}
</Mutation>
</React.Fragment>
);
};
export default compose(
reduxForm({
form: "skillsProfile",
enableReinitialize: true,
destroyOnUnmount: false
})
)(Skills);
why optimistic ui update is not working here when Query is done in background component?
From the docs:
Sometimes when you perform a mutation, your GraphQL server and your Apollo cache become out of sync. This happens when the update you’re performing depends on data that is already in the cache; for example, deleting and adding items to a list. We need a way to tell Apollo Client to update the query for the list of items. This is where the update function comes in!
There's no way for Apollo to know what sort of operations your server is doing when it executes a mutation (adding one or more rows, deleting a row, etc.) -- all it has to go off is the data that's returned by the mutation. It can match this data against objects that are already in the cache and update them accordingly, but that's it. If there are any fields that were cached and need to be updated as a result of your mutation, you need to explicitly tell Apollo how to do this (side note, these could be fields that return a List of Skills, but they could be literally any other fields that were impacted by the mutation).
Your update function for adding a skill, for example, would look something like this:
update={(cache, { data: { addSkill } }) => {
// assumes addSkill resolves to a single Skill
const { skills, ...rest } = cache.readQuery({ query: BACKGROUND_QUERY });
cache.writeQuery({
query: BACKGROUND_QUERY,
data: { skills: skills.concat([addSkill]), ...rest },
});
}}
See the docs for additional examples. It's also worthwhile noting that when using readQuery or writeQuery, you will need to pass in the appropriate variables if your query takes any.
While you can refetch your queries (for example, by specifying them as part of refetchQueries), it's largely unnecessary for simple updates to the cache and obviously slower than just using update since it requires another round-trip to your server. Additionally, update even works with optimistic updates to your UI.

How do I iterate over an array with a map function, infinitely?

I am building a React JS application.
So, I want to print something over and over from an array, but it has only two elements. I am using a custom package called 'Typist' that enables me to give a 'Typing' kind of animation with whatever I type.
I am basically trying to type 'Hi', erase it and then type 'Ola' and then erase it and then start with 'Hi' again and keep repeating this pattern.
Here's what I have right now:
let greetings=["Hi","Ola"];
render() {
return(
<div className={"TypistExample-header"} >
<Typist className={"TypistExample"}>
<Typist.Delay ms={1000} />
{
greetings.map(i =>{
return <li><h1>{i}</h1>
{<Typist.Backspace count={12} delay={1000} />}
</li>
})
}
</Typist>
P.S I did find a way to do it a few times,still not infinite, by doing this:
let greetings=["Hi","Ola"];
var actualTyping= greetings.map(i =>{
return <li><h1>{i}</h1>
{<Typist.Backspace count={12} delay={1000} />}
</li>
});
var rows=[];
for(var i=0;i<10;i++){
rows.push(actualTyping)
}
return(
<div className={"TypistExample-header"} >
<Typist className={"TypistExample"}>
<Typist.Delay ms={1000} />
{rows}
</Typist>
</div>
);
You can use Typist's onTypingDone property to restart the animation. Pass the text array via state to Typist. When the typing is done, clear the state, which will remove the rendered Typist, then restore the original text, and it will be typed again (sandbox).
Note: setState is asynchronous, and it batches updates together or defers them to a later time. In this case we want to clear the text (set null), and only after the view is updated repopulate the text. To do so, we can use the 2nd param of setState, a callback that is fired only after the update (null) has been applied.
const greetings = ["Hi", "Ola"];
class ContType extends React.Component {
constructor(props) {
super(props);
this.state = {
text: props.text
};
}
onTypingDone = () => {
this.setState(
{
text: null
},
() =>
// run this callback after the state updates
this.setState({
text: this.props.text
})
);
};
render() {
const { text } = this.state;
return (
text && (
<Typist onTypingDone={this.onTypingDone}>
<Typist.Delay ms={1000} />
{text.map((i, index) => {
return (
<div key={index}>
<h1>{i}</h1>
{<Typist.Backspace count={12} delay={1000} />}
</div>
);
})}
</Typist>
)
);
}
}
render(<ContType text={greetings} />, document.getElementById("root"));
Better and simple solution would be creating a constant integer array of your choice and then mapping carried out using the value specified for integer.
const a = [1...10000]
let greetings = ["hi", "hello"]
render(){
return(
a.map( i => {
<h1>greeting[0] - greeting[1]</h1>
})
)
}
And always, keep in mind that infinite loop cause react engine to break down. Good practice is to specify an integer value for mapping for such cases.

Filtering array elements in React/Redux

I have a React application which filters users and posts. My navbar can link to either users, posts.
Clicking on posts brings up a list of every post made, by all users (with some extra meta information for each one).
Here is my Posts component:
export default class Posts extends Component {
render() {
return (
<div>
{this.props.posts.map( post =>
<Post {...this.props} post={post} key={post.id} />
)}
</div>
)
}
}
On the users page, each user has a sublink posts. What I want is to be able to click on posts for that user and it to take you to a posts page (like above), but only list the posts for that user.
The posts array looks a bit like this:
[
{ id: 1,
userId: 1
},
{ id: 2,
userId: 1
},
{ id: 3,
userId: 2
},
{ id: 4,
userId: 3
},
{ id: 5,
userId: 3
}
]
Say I clicked on the user with userId of 3, and only wanted to display his/her posts. What is the best way to do this?
I know I can use react-router to pass the userId to the URL, and then pull that back into the component, but I'm not sure how to use this once the Posts component renders. Ultimately I want it to render the posts array filtered to the userId route param, else render all posts. Is this right approach?
Any help appreciated.
UPDATE:
Ok, I know this is not an elegant solution, but I want to start off with a solution, then I will 'trim it down', so to speak.
There's a bug in this code which I cannot figure out:
render() {
const {userId} = this.props.params
if(userId) {
const postsForUser = posts.filter( post => post.userId === userId )
return (
<div>
{postsForUser.map( post =>
<Post {...this.props} post={post} key={post.id} />
)}
</div>
)
}
else {
return (
<div>
{this.props.posts.map( post =>
<Post {...this.props} post={post} key={post.id} />
)}
</div>
)
}
} // end render()
When I log userId, it correctly logs the user id that I have just clicked on, so my this.props.params.userId variable is working correctly.
If I define postsForUser with a hardcoded integer, e.g.
const postsForUser = posts.filter( post => post.userId === 3 )
It returns the filtered array correctly.
So at this point it seems everything is working fine, except when I swap out the hardcoded id for the variable, postsForUser returns as an empty array.
Where am I going wrong?
UPDATE 2 (Fix):
For anyone facing this problem, I incorrectly returned the id from this.props.params.userId as a string, so instead I had to define it using parseInt() like this:
const userId = parseInt(this.props.params.userId)
You can simply use Array's filter method:
render(){
return (
<div>
{
if(this.params.userId) {
const postsForUser = posts.filter(post => post.userId == this.params.userId);
//renderPostList(postsForUser)
} else {
//renderPostList(posts)
}
}
</div>
)
}

Categories

Resources