Framer Motion reorder failing silently with nested items - reactjs

I have an array of questions that I'm trying to animate using Framer Motion. The shape roughly looks like:
const questionList = [
{
type: "text",
data: {
question: "What is your name?",
valid: true,
},
},
{
type: "text",
data: {
question: "How old are you?",
valid: true,
},
},
{
type: "multi",
data: {
question: "What are your favorite sports?",
responses: ["Football", "Hockey", "Tennis", "Chess"],
valid: true,
},
},
{
type: "single",
data: {
question: "What is your gender?",
responses: ["Male", "Female", "Don't know"],
valid: true,
},
},
];
I am rendering them in a list of cards using React and Tailwind like so:
export default function QuestionList() {
const [questions, setQuestions] = React.useState(questionList);
return (
<Reorder.Group values={questions} onReorder={setQuestions}>
{questions.map((question, index) => {
return (
<Reorder.Item
className="mb-4 px-4 py-6 bg-indigo-500 text-white ml-12 border-2 border-gray-100 shadow rounded-md cursor-move hover:shadow-md"
key={`question${question.type}${index}`}
value={`question${question.type}${index}`}>
{question.data.question}
</Reorder.Item>
);
})}
</Reorder.Group>
);
}
I'd like to be able to reorder the cards using Framer Motion Reorder components as described here https://www.framer.com/docs/reorder/ but every time I try the component tree crashes silently and I get a blank screen. When I reduce the questions to a flat structure like ['Question 1', 'Question 2'] etc I am able to get the re-ordering to happen. I suspected it could be something to do with the keys but playing around with that doesn't work. Grateful for any help/pointers

You shouldn't use the loop index as the key in your Reorder.Item. When you drag to reorder the item, the index (and thus the key) will change. That will make it impossible for Reorder to track which elements have been moved to where and is probably why it's crashing.
Instead use a value like a unique id property for each question. This way React (and Framer Motion) will know exactly which element was moved and can render it at the new position.
Here's a more thorough explanation:
react key props and why you shouldn’t be using index

Besides the key having to be unique, you also should set value from Reorder.Item to value={question}. If you want to generate a unique ID for each question, perhaps consider using a library such as uuidv4

Related

Does a complex/deeply nested client-side state need to be normalised?

Background
I am building a single-page-application in React whose data will be retrieved from a relational database. Consider the following two tables:
menu
menu_items
A menu has many menu items. Menu items can be related to other menu items (represented in the database as an adjacency list). On the client, I'm representing it as a tree, i.e.:
{
"id": "menu",
"items": [
{
"id": "item-1",
"name": "Breakfast",
"children": []
},
{
"id": "item-2",
"name": "Lunch",
"children": [{ "id": "item-2-1", "children": [] }]
}
]
}
UI
A tree can get four levels deep and is typically much wider than it is tall. It is currently rendered recursively in the following way:
type Properties = {
items: {
id: string;
name: string;
children: Properties["items"];
}[];
};
const Items = ({ items }: Properties) => (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name}
<Items items={item.children} />
</li>
))}
</ul>
);
Problem
I have reached the stage where I want to update specific nodes in the tree. This operation seems complex, because it involves searching and replacing entire subtrees. Additionally, it will happen often, i.e. onChange, as a user updates item.name.
Although I don't use Redux, the following article explains it could be better to normalise nested client-side data to make operations like this easier: https://redux.js.org/usage/structuring-reducers/normalizing-state-shape.
Example
const menu = {
"id": "menu",
"itemMap": {
"item-1": { "parentId": null },
"item-2": { "parentId": null },
"item-2-1": { "parentId": "item-2" }
}
}
Question
Would I not have to denormalise/turn it back into a tree to render the UI? If yes, is there any point in my normalising the data?
I don't have a lot of experience with this and am struggling to find the right resources to answer the questions I have.
As with most engineering problems, there isn't a "correct" answer — rather it is a tradeoff: it depends on the expected use.
Your current approach optimizes for the maximum render performance at the cost of mutation performance. By using a tree structure, no transformation is needed at render time (just iteration) — however, arbitrary node lookups within the tree can't be done in constant time.
Another approach is to store the data as an associative array of nodes (Node ID ➡️ Node — e.g. Object/Map), which will optimize for arbitrary node lookup — and you can simply build the tree on every render by including each node's child IDs as part of its structure.
Here's an example of such a structure using the data that you provided:
TS Playground
<script src="https://cdn.jsdelivr.net/npm/#babel/standalone#7.20.15/babel.min.js"></script><script>Babel.registerPreset("tsx", {presets: [[Babel.availablePresets["typescript"], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx">
/** Any application-specific data type must satisfy this */
type ValidNode = {
/** Unique */
id: string;
/** Not allowed because of conflict */
children?: never;
};
// Your application-specific data type:
type Item = {
id: string;
name: string;
// etc.
};
type ListNode<T extends ValidNode> = T & {
/** List of child IDs */
children?: string[] | undefined;
};
// An object consisting of ID keys and ListNode values:
type ListNodeMap<T extends ValidNode> = Partial<Record<string, ListNode<T>>>;
const nodeMap: ListNodeMap<Item> = {
"menu": {
id: "menu",
name: "Menu",
children: ["item-1", "item-2"],
},
"item-1": {
id: "item-1",
name: "Breakfast",
},
"item-2": {
id: "item-2",
name: "Lunch",
children: ["item-2-1"],
},
"item-2-1": {
id: "item-2-1",
name: "Irresistibly healthy",
},
};
// A type-safe getter function which throws on bad IDs:
function getNode (
nodeMap: ListNodeMap<Item>,
id: string,
): ListNode<Item> {
const node = nodeMap[id];
if (!node) throw new Error(`Node ID ${JSON.stringify(id)} not found`);
return node;
}
/** The "linked" version of a ListNode */
type TreeNode<T extends ValidNode> = T & {
children?: T[] | undefined;
};
// Note: This uses recursion because it's for tree structures.
// Calling with list nodes having cyclic reference IDs will create an infinite loop.
function createTree (
nodeMap: ListNodeMap<Item>,
id: string,
): TreeNode<Item> {
const node = getNode(nodeMap, id);
return {
...node,
children: node.children?.map(id => createTree(nodeMap, id)),
};
}
console.log("node map:", nodeMap);
console.log("tree:", createTree(nodeMap, "menu"));
</script>
You didn't show how you receive the data, but if your API returns nodes with parent IDs instead of child IDs, then you can simply lookup each parent when acquiring new children and insert the child IDs at the time of acquisition — using an intermediate mapping structure if needed... that's tangential to the asked question.
You also didn't show how you plan to update node names, so I've excluded that part in the linked playground below, but here's an otherwise complete example of the code above with state and a reducer for updating an arbitrary node: Full example with state and reducer in TS Playground

Is it bad practice to access HTML elements by ID in React TypeScript?

I was told at a previous job that I should never access HTML elements directly through means like getElementById in React TypeScript. I'm currently implementing Chart.js. For setting up the chart, I was initially using a useRef hook instead of accessing context, but now it seems like I need to grab the canvas by ID in order to instantiate it properly. I want to know if this is kosher.
I suspect something is wrong with me not using a context, because my chart data doesn't load and throws a console error: "Failed to create chart: can't acquire context from the given item"
useEffect(() => {
chart = new Chart(chartRef.current, {
type: "bar",
data: {
labels: labelsArray.map((label) => {
const date = new Date(label);
// add one because month is 0-indexed
return date.getUTCMonth() + 1 + "/" + date.getUTCDate();
}),
datasets: [
{
data: valuesArray,
backgroundColor: "#1565C0",
borderRadius: 6,
},
],
},
options: {
interaction: { mode: "index" },
onHover: (event, chartElement) => {
const target = event.native.target;
(target as HTMLInputElement).style.cursor = chartElement[0]
? "pointer"
: "default";
},
plugins: {
tooltip: {
mode: "index",
enabled: true,
},
title: {
display: true,
text: "Daily Usage Past 30 Days",
align: "start",
fullSize: true,
font: {
size: 24,
},
padding: {
bottom: 36,
},
},
},
scales: {
x: {
display: false,
},
},
elements: {
line: {
borderJoinStyle: "round",
},
},
},
});
return () => {
chart.destroy();
};
}, [labelsArray, valuesArray]);
and HTML:
<div className="mt-80 ml-12 p-8 shadow-lg border-2 border-slate-100 rounded-xl items-center">
<canvas id="chart" ref={chartRef}></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</div>
Also, per the Chart.js documentation: "To create a chart, we need to instantiate the Chart class. To do this, we need to pass in the node, jQuery instance, or 2d context of the canvas of where we want to draw the chart." Not sure how we would do this with a useRef
Yes,
It is not good practice to access dom elements directly through document API.
Because in react
virtual dom is responsible for painting/ re-rendering the UI.
State updation is the proper way to tell react to trigger re-render.
The flow is state updation -> calculate differences -> find who over is using that state -> grab those components -> re-render only those components.
virtual dom is the source of truth for react to render and update actual DOM.
Now, If you directly access some dom elements and do some operation on it, like updating, react will never know that some change has happened and it will basically break the flow of the react, in which case there will be no reason to use react.js
The flow would be accessing some dom element -> updating -> displaying.
The problem with this approach if react encounters that later what i have in virtual dom is not actual presentation in the actual dom, which will create mess.
That is the reason there is useRef hook to manipulate dom.

How to display images from json URL when the images key has two object values

This is my first stackoverflow post....be kind
I am building a react app where I want to display an image from a array of objects but the "images:" object has two objects inside example:
[
{ value: "all", label: "All sets" },
{
value: "base1",
label: "Base",
series: "Base",
printedTotal: 102,
total: 102,
legalities: { unlimited: "Legal" },
ptcgoCode: "BS",
releaseDate: "1999/01/09",
updatedAt: "2020/08/14 09:35:00",
images: {
symbol: "https://images.pokemontcg.io/base1/symbol.png",
logo: "https://images.pokemontcg.io/base1/logo.png",
},
},]
my question is how would I access the inner object to display just the "logo:" object?
my assumption is to use a combination of .map and Object.keys() but I'm unsure how to do it.
mycode is below...
import React from "react";
import setOptions from "../data/sets";
export default function SetOverview() {
const sets = setOptions;
return (
<div>
{sets.map((set) => {
return (
<div key={set.value}>
<h2>{set.label}</h2>;<p>Number of cards: {set.printedTotal}</p>
<p>Release date: {set.releaseDate}</p>
</div>
// image to be placed here
);
})}
</div>
);```
ma friend. Welcome to Stack Overflow, and no- you will not be treated with kindness. That being said,
If you are sure about the keys that are being passed in the dataset, i.e.: objName.objItem.images.logo, then all you need to do is,
Check if the images key exists in that object (because your first object doesn't have it, so I suspect there may be a reason for that).
Load the image's logo value inside that div you've specified.
To achieve this, all you need to do is:
set.images?.logo && <img src={set.images.logo} />
And voila, you shall have your image. The question mark checks if key exists.

How do I select and update an object from a larger group of objects in Recoil?

My situation is the following:
I have an array of game objects stored as an atom, each game in the array is of the same type and structure.
I have another atom which allows me to store the id of a game in the array that has been "targeted".
I have a selector which I can use to get the targeted game object by searching the array for a match between the game ids and the targeted game id I have stored.
Elsewhere in the application the game is rendered as a DOM element and calculations are made which I want to use to update the data in the game object in the global state.
It's this last step that's throwing me off. Should my selector be writable so I can update the game object? How do I do this?
This is a rough outline of the code I have:
export const gamesAtom = atom<GameData[]>({
key: 'games',
default: [
{
id: 1,
name: 'Bingo',
difficulty: 'easy',
},
{
id: 21,
name: 'Yahtzee',
difficulty: 'moderate',
},
{
id: 3,
name: 'Twister',
difficulty: 'hard',
},
],
});
export const targetGameIdAtom = atom<number | null>({
key: 'targetGameId',
default: null,
});
export const targetGameSelector = selector<GameData | undefined>({
key: 'targetGame',
get: ({ get }) => {
return get(gamesAtom).find(
(game: GameData) => game.id === get(selectedGameIdAtom)
);
},
// This is where I'm getting tripped up. Is this the place to do this? What would I write in here?
set: ({ set, get }, newValue) => {},
});
// Elsewhere in the application the data for the targetGame is pulled down and new values are provided for it. For example, perhaps I want to change the difficulty of Twister to "extreme" by sending up the newValue of {...targetGame, difficulty: 'extreme'}
Any help or being pointed in the right direction will be appreciated. Thanks!

How to specify a key for React children when mapping over an array

I have a method in a react Contact List component where I am returning another component. I have got it working but am curious if there is a better way to structure how I am using the key.
Specifically - I'm asking about this line of code from the method below (data is hard coded as sample to get started):
return <ShortContact contact={contact} key={contact.id}/>
Here is the code in context:
_getContacts() {
let contactList = [
{
id: 1,
fName: "aaa",
lName: "aaaaa",
imgUrl: "http://brainstorminonline.com/wp-content/uploads/2011/12/blah.jpg",
email: "aaa#aaaa.com",
phone: "999999999999"
},
{
id: 2,
fName: "bbbbb",
lName: "bbbbbbb",
imgUrl: "https://media.licdn.com/mpr/mpr/shrinknp_200_200/bbb.jpg",
email: "bbb#bbb-bbb.com",
phone: "888888888888"
},
{
id: 3,
fName: "Number",
lName: "Three",
imgUrl: "http://3.bp.blogspot.com/-iYgp2G1mD4o/TssPyGjJ4bI/AAAAAAAAGl0/UoweTTF1-3U/s1600/Number+3+Coloring+Pages+14.gif",
email: "three#ccccc.com",
phone: "333-333-3333"
}
];
return contactList.map((contact) => {
"use strict";
return <ShortContact contact={contact} key={contact.id}/>
});
}
ShortContact Component Render:
class ShortContact extends React.Component {
render() {
return (
<div >
<li className="contact-short well whiteBG">
<img className="contact-short-thumb" src={this.props.contact.imgUrl}/>
<p className="contact-short-name">{this.props.contact.fName}</p><br />
<p className="contact-short-email">{this.props.contact.email}</p>
</li>
</div>
);
}
}
I struggled with how to make it work and not get the warning Warning: Each child in an array or iterator should have a unique "key" prop. However I am wondering if the syntax or structure is valid and if it should be refactored.
There is nothing wrong with this code. The key is required so that react knows how to render the children nodes. In fact your implementation is exactly what react requires the programmer to do. Now the details of which key to use and such can be changed, but it looks like you have the most performant solution already.
The main requirement is that the key is unique so as long as contact.id is always unique (which if its coming from a database then it will be) then you are fine.
Alternatively you can use an index on your map for the key but I wouldn't really recommend it (i'll explain below after the code snippet).
contactList.map((contact, i) => {
return <ShortContact contact={contact} key={i}/>
});
Personally I think your approach is the best approach because it can prevent additional renders. What I mean is for instance when a new contact is returned from the server every contact row would be re-rendered because the index in the array for each contact is different (assuming you aren't treating it like a stack)... The different index with new contact data at that index would cause the re-render. Because contact.id is a static number if the data for that contact hasn't changed then react wont re-render it.
The use of a unique key is required. In your example, using the id is ideal. see the following for more information:
https://facebook.github.io/react/docs/lists-and-keys.html

Resources