Reputation: 65
What I'm trying to do
I have a main component with an array of "profiles". For each profile, I have two different CONDITIONAL components (only one is shown at once). Each of these two components has a button that is supposed to switch the components when clicked. So I have lifted the state up to the main component, created component state using the 'useState' hook (which is an array, where each index is a string representing the child component to be shown for each element in the profiles array), I've created two functions for event handling for these clicks, and passed them into their child components as render props.
They start out as the first child component.
The Problem & How I found it
When you press the button to switch to the other component, it works. When you press the button to go back, it crashes. Saying "TypeError: Cannot assign to read-only property '0' of string 'large'". I put some console.log(state) after the useState initialization, and after the state change calls in each function. What happens (with a test list with just one element) is that
Main Component
const Peers = props => {
let dummyPeer = {
_id: "9asdf98sj3942j4fs9ji",
user: {
name: "Test Peer",
avatar: "//www.gravatar.com/avatar/cd56136f6d9abfdf4a0198dc9ce656c8?s=200&r=pg&d=mm"
},
bio: "Biography for Test Peer",
year: "2022",
courses: [
"CISC124",
"PSYC223",
"PSYC236",
"COMM200",
"CISC251"
]
}
let profiles = [];
profiles.push(dummyPeer);
let initialState = [];
profiles.forEach(profile => {
initialState.push("normal");
});
let [viewState, setViewState] = useState(initialState);
console.log(viewState);
const openLargeView = (id) => {
let changeIndex = profiles.map(profile => profile._id).indexOf(id);
setViewState(state => state[changeIndex] = "large");
console.log(viewState);
}
const closeLargeView = (id) => {
let changeIndex = profiles.map(profile => profile._id).indexOf(id);
setViewState(state => state[changeIndex] = "normal");
console.log(viewState);
}
return (
<Fragment>
{profiles.map((profile, index) => (<Fragment key={profile._id} >
{viewState[index] === "normal" ? (
<Peer openLargeView={openLargeView} profile={profile} />
) : (
<ViewPeer closeLargeView={closeLargeView} profile={profile} />
)}
</Fragment>))}
</Fragment>
)
}
Child Component 1:
const Peer = ({ profile, openLargeView }) => {
const { _id, user, bio, year, courses } = profile;
const { avatar } = user;
return (<Fragment>
<div className="card-row">
<div className="profile-header">
<h1 className="peer-text row-title"> {user.name} </h1>
<p className="peer-text peer-small"> {year} </p>
<img className="avatar avatar-peer-small" src={avatar} alt='' />
</div>
<button onClick={() => openLargeView(_id)} className="btn-small"> More </button>
</div>
</Fragment>)
}
Child Component 2:
const ViewPeer = ({ profile, closeLargeView }) => {
const { _id, user, bio, year, courses } = profile;
const { avatar } = user;
let courseElements = courses.map((course, index) =>
<li key={index} className="profile-text"> {course} </li>
);
return (
<Fragment>
<div className="card-md peer-card">
<div className="profile-header">
<h1 className="peer-text"> {user.name} </h1>
<img className="avatar avatar-peer" src={avatar} alt='' />
</div>
<div className="profile-info">
<h2 className="profile-text"> {bio} </h2>
<h2 className="profile-text2"> Year: {year} </h2>
<ul className="course-list"> {courseElements} </ul>
<div className="profile-button-group">
<button onClick={() => closeLargeView(_id)} className="btn-small"> Close </button>
<button className="btn-small"> Send Buddy Request </button>
</div>
</div>
</div>
</Fragment>
)
}
Expected and actual results
I expect it to go back to the original component when the button of the first component is clicked but the state turned into an array to a string and the app crashes.
Upvotes: 2
Views: 213
Reputation: 30370
The problem here is the way viewState
is being updated in openLargeView()
and closeLargeView()
.
When those functions are called, the call to setViewState
invokes the state change callback that actually changes the type of the viewState
from Array to String:
/*
Summary of problem with following line of code:
1. The statement: state[changeIndex] = "large" returns the string "large"
2. When executed, the statement returns the "large" string from the callback
3. The viewState therefore becomes a string with value "large"
*/
setViewState(state => state[changeIndex] = "large");
Consider revising these state updates to something like this:
setViewState(state => {
/*
1. Shallow clone state into a new array with ... spread
2. Assign value of "large" to the "changeIndex" in cloned array
3. Return cloned array as new state for viewState
*/
const arrayClone = [...state];
arrayClone[changeIndex] = "large";
return arrayClone;
});
This ensures that the state passed back to your component by the setViewState()
callback is of the array type, which is what your component is expecting. A more complete example showing were all changes are needed would be:
const Peers = props => {
const profiles = [{
_id: "9asdf98sj3942j4fs9ji",
user: {
name: "Test Peer",
avatar: "//www.gravatar.com/avatar/" +
"cd56136f6d9abfdf4a0198dc9ce656c8?s=200&r=pg&d=mm"
},
bio: "Biography for Test Peer",
year: "2022",
courses: [
"CISC124",
"PSYC223",
"PSYC236",
"COMM200",
"CISC251"
]
}]
let [viewState, setViewState] = useState(["normal"]);
const openLargeView = (id) => {
let changeIndex = profiles.map(profile => profile._id).indexOf(id);
setViewState(state => {
const arrayClone = [...state];
arrayClone[changeIndex] = "normal";
return arrayClone;
});
}
const closeLargeView = (id) => {
let changeIndex = profiles.map(profile => profile._id).indexOf(id);
setViewState(state => {
const arrayClone = [...state];
arrayClone[changeIndex] = "large";
return arrayClone;
});
}
return (
<Fragment>
{profiles.map((profile, index) => (<Fragment key={profile._id} >
{viewState[index] === "normal" ? (
<Peer openLargeView={openLargeView} profile={profile} />
) : (
<ViewPeer closeLargeView={closeLargeView} profile={profile} />
)}
</Fragment>))}
</Fragment>
)
}
Hope that helps!
Upvotes: 2