I don't know if I'm following an advisable/correct pattern but basically, my application leverages on same arrays used in various components.
I am thinking about generalizing these lists as constants for simplicity/maintainability.
Here follows an example: I have two different arrays with fixed labels but dynamic contents. The first array retrieves the content info using the "props" I am passing to the contents while the second one uses new data fetched with useEffects.
Note that I render these arrays in many different ways so it may be better to not fix the rendering.
import {useState, useEffect} from "react";
export default function App({ props }: Props) {
const [dataIntegration, setDataIntegration] = useState(null);
useEffect(() => {
const fetchDataIntegration = async () => {
const { data, error } = await client
.from("someTable").select("*").eq("id", props.id);
if (error) {
console.error(error);
return;
}
if (data) {
setDataIntegration(data[0]);
}
};
fetchDataIntegration();
}, []);
const items1 = [
{ fixedLabel: "Fixed Label 1", variableContent: props?.content1 },
{ fixedLabel: "Fixed Label 2", variableContent: props?.content2 },
];
const items2 = [
{ fixedLabel: "Fixed Label 1", variableContent: dataIntegration?.content1 },
{ fixedLabel: "Fixed Label 2", variableContent: dataIntegration?.content2 },
];
return (
<div>
<div>
{items1.map((item1) => (
<div key={item2.fixedLabel}>
<div>
{item1.variableContent}
</div>
</div>
))}
</div>
<div>
{items2.map((item2) => (
<div key={item2.fixedLabel}>
<div>
{item2.variableContent}
</div>
</div>
))}
</div>
</div>
);
}
As per my experience, when working on big codebases it's generally a good idea to put these arrays or any constant data in a constant file for ease of maintainability.
You are correct in wanting to generalize these. Whenever you have code that is very repetitive - as the items1, items2, etc suggests - you should try to simplify.
So instead of doing:
const items1 = [
{ fixedLabel: "Fixed Label 1", variableContent: props?.content1 },
{ fixedLabel: "Fixed Label 2", variableContent: props?.content2 },
];
const items2 = [
{ fixedLabel: "Fixed Label 1", variableContent: dataIntegration?.content1 },
{ fixedLabel: "Fixed Label 2", variableContent: dataIntegration?.content2 },
];
It would be better maintainable and reusable like this:
const item = (label, variabeleContent) => {
return {
label: label,
content: variabeleContent,
};
}
From there, you can fill up array with items or call them manually.
Related
I'm using React Hook Form to build a basic page builder application and it's been brilliant so far, I've been using the useFieldArray hook to create lists that contain items, however, I haven't found a way to move items between lists.
I know I can use the move() function to reorder items within the same list, however, since each list has its own nested useFieldArray I can't move the item from one list component to another list component.
If anyone knows of a way around this it would be much appreciated!
Here is a very simplified example of my current setup:
export const App = () => {
const methods = useForm({
defaultValues: {
lists: [
{
list_id: 1,
items: [
{
item_id: 1,
name: 'Apple'
},
{
item_id: 2,
name: 'Orange'
}
]
},
{
list_id: 2,
items: [
{
item_id: 3,
name: 'Banana'
},
{
item_id: 4,
name: 'Lemon'
}
]
}
]
}
});
return (
<FormProvider {...methods}>
<Page/>
</FormProvider>
)
}
export const Page = () => {
const { control } = useFormContext();
const { fields } = useFieldArray({
control,
name: 'lists'
})
return (
<ul>
{fields?.map((field, index) => (
<List listIdx={index} />
))}
</ul>
)
}
export const List = ({ listIdx }) => {
const { control, watch } = useFormContext();
const { fields, move } = useFieldArray({
control,
name: `lists[${sectionIdx}].items`
})
const handleMove = (prevIdx, nextIdx) => {
// this allows me to move within lists but not between them
move(prevIdx, nextIdx);
}
return (
<li>
<p>ID: {watch(lists[${listIdx}].list_id)}</p>
<ul>
{fields?.map((field, index) => (
<Item listIdx={index} itemIdx={index} handleMove={handleMove}/>
))}
</ul>
</li>
)
}
export const Item = ({ listIdx, itemIdx, handleMove }) => {
const { control, register } = useFormContext();
return (
<li>
<p>ID: {watch(lists[${listIdx}].items[${itemIdx}].item_id)}</p>
<label
Name:
<input { ...register('lists[${listIdx}].items[${itemIdx}]) }/>
/>
<button onClick={() => handleMove(itemIdx, itemIdx - 1)}>Up</button>
<button onClick={() => handleMove(itemIdx, itemIdx + 1)}>Down</button>
</div>
)
}
Thanks in advance!
If you'd not like to alter your default values (your data structure), I think the best way to handle this is using update method returning from useFieldArray. You have the data of both inputs that are going to be moved around, knowing their list index and item index, you could easily update their current positions with each other's data.
When the user clicks on a list item, I want it to render the paragraph content for that list item. When the user clicks on another list item, I then want to erase the previous content and register then new paragraph data for the new list item. Currently, when I click the list items, the info data stays for all and never goes away. Does this make sense? If you go to this website https://brittanychiang.com/ and click the "experience" button in the nav, you can see the functionality I am going for.
Questions :
import SingleQuestion from "./SingleQuestion";
import classes from "./Question.module.css";
const questions = [
{
id: 1,
title: "Step 1",
info: "paragraph 1 text",
},
{
id: 2,
title: "Step 2",
info: "paragraph 2 text",
},
{
id: 3,
title: "Step 3",
info: "paragraph 3 text",
},
{
id: 4,
title: "Step 4",
info: "paragraph 4 text",
},
];
const Questions = () => {
return (
<main>
<h1 className="infoTitle">We have answers</h1>
<div className={classes.container}>
{questions.map((question) => {
return <SingleQuestion key={question.id} {...question} />;
})}
</div>
</main>
);
};
export default Questions;
SingleQuestion:
import React, { useState } from "react";
import classes from "./Question.module.css";
const SingleQuestion = ({ title, info }) => {
const [text, setText] = useState("");
const showTextHandler = () => {
setText(info);
};
return (
<section className={classes.elementBin}>
<ul>
<li className={classes.hi} onClick={showTextHandler}>
{title}
</li>
</ul>
<p>{text}</p>
</section>
);
};
export default SingleQuestion;
There are plenty of ways of solving this, I will just show an example that does not differ so much from your current code:
const Questions = () => {
const [selectedId, setSelectedId] = useState();
return (
<main>
<h1 className="infoTitle">We have answers</h1>
<div className={classes.container}>
{questions.map((question) => {
return (
<SingleQuestion
key={question.id}
isSelected={question.id === selectedId}
setSelectedId={setSelectedId}
{...question}
/>
);
})}
</div>
</main>
);
};
const SingleQuestion = ({ id, title, info, isSelected, setSelectedId }) => {
return (
<section className={classes.elementBin}>
<ul>
<li className={classes.hi} onClick={() => setSelectedId(id)}>{title}</li>
</ul>
{isSelected && <p>{info}</p>}
</section>
);
};
As you can see, an upper state was created, holding the selected question id.
The setter for that state is passed to the SingleQuestion component.
Now that component does not hold an internal state anymore, it just update the selected question id, and shows its info just if is the selected one.
After implementing the drag and drop feature on AG Grid table, I'm looking for a way to get the current state with the updated order/index of rows. My goal is to persist the table data after changing the order, but can't find the respective state of the current order.
I'd appreciate any help or any idea.
Sandbox demo and example code below
import React from "react";
import { AgGridReact } from "ag-grid-react";
import "ag-grid-community/dist/styles/ag-grid.css";
import "ag-grid-community/dist/styles/ag-theme-alpine.css";
function App() {
const [gridApi, setGridApi] = React.useState(null);
const [gridColumnApi, setGridColumnApi] = React.useState(null);
const onGridReady = (params) => {
setGridApi(params.api);
setGridColumnApi(params.columnApi);
};
const defaultColDef = {
flex: 1,
editable: true
};
const columnDefs = [
{
headerName: "Name",
field: "name",
rowDrag: true
},
{ headerName: "stop", field: "stop" },
{
headerName: "duration",
field: "duration"
}
];
const rowData = React.useMemo(
() => [
{
name: "John",
stop: 10,
duration: 5
},
{
name: "David",
stop: 15,
duration: 8
},
{
name: "Dan",
stop: 20,
duration: 6
}
],
[]
);
return (
<div>
<h1 align="center">React-App</h1>
<div>
<div className="ag-theme-alpine" style={{ height: "700px" }}>
<AgGridReact
columnDefs={columnDefs}
rowData={rowData}
defaultColDef={defaultColDef}
onGridReady={onGridReady}
rowDragManaged={true}
></AgGridReact>
</div>
</div>
</div>
);
}
export default App;
You can get the order of the rows inside the grid by iterating over them using the Grid API method forEachNode:
API for Row Nodes
const rows = [];
gridApi.forEachNodeAfterFilterAndSort((node) => rows.push(node.data));
console.log(rows);
See this implemented in the following sample.
You're currently using managed dragging by passing rowManagedDragging={true}, which means the AgGridReact component is managing the row order state.
If you want to maintain row order state outside the component, you need to use Unmanaged Dragging.
Add a handler for onRowDragMove, and use the node and overIndex or overNode properties of the event to update your local event order state, and pass it to the AgGridReact component to re-render.
Take a look at this example from the docs
I am trying to use react-select in combination with match-sorter as described in this stackoverflow answer (their working version). I have an initial array of objects that get mapped to an array of objects with the value and label properties required by react-select, which is stored in state. That array is passed directly to react-select, and when you first click the search box everything looks good, all the options are there. The onInputChange prop is given a call to matchSorter, which in turn is given the array, the new input value, and the key the objects should be sorted on. In my project, and reproduced in the sandbox, as soon as you type anything into the input field, all the options disappear and are replaced by the no options message. If you click out of the box and back into it, the sorted options show up the way they should. See my sandbox for the issue, and here's the sandbox code:
import "./styles.css";
import { matchSorter } from "match-sorter";
import { useState } from "react";
import Select from "react-select";
const objs = [
{ name: "hello", id: 1 },
{ name: "world", id: 2 },
{ name: "stack", id: 3 },
{ name: "other", id: 4 },
{ name: "name", id: 5 }
];
const myMapper = (obj) => {
return {
value: obj.id,
label: <div>{obj.name}</div>,
name: obj.name
};
};
export default function App() {
const [options, setOptions] = useState(objs.map((obj) => myMapper(obj)));
return (
<Select
options={options}
onInputChange={(val) => {
setOptions(matchSorter(options, val, { keys: ["name", "value"] }));
}}
/>
);
}
I am sure that the array in state is not getting removed or anything, I've console logged each step of the way and the array is definitely getting properly sorted by match-sorter. It's just that as soon as you type anything, react-select stops rendering any options until you click out and back in again. Does it have something to do with using JSX as the label value? I'm doing that in my project in order to display an image along with the options.
I had to do two things to make your code work:
Replaced label: <div>{obj.name}</div> with label: obj.name in your mapper function.
I am not sure if react-select allows html nodes as labels. Their documentation just defines it as type OptionType = { [string]: any } which is way too generic for anything.
The list supplied to matchSorter for matching must be the full list (with all options). You were supplying the filtered list of previous match (from component's state).
const objs = [
{ name: "hello", id: 1 },
{ name: "world", id: 2 },
{ name: "stack", id: 3 },
{ name: "other", id: 4 },
{ name: "name", id: 5 }
];
const myMapper = (obj) => {
return {
value: obj.id,
label: obj.name, // -------------------- (1)
name: obj.name
};
};
const allOptions = objs.map((obj) => myMapper(obj));
export default function App() {
const [options, setOptions] = useState(allOptions);
return (
<Select
options={options}
onInputChange={(val) => {
setOptions(
matchSorter(
allOptions, // ----------------> (2)
val,
{ keys: ["name", "value"]
}
));
}}
/>
);
}
I am trying to get my head around props and how they work exactly. Here is my layout so far.
I have created a page called "TodoData.js" which has all of my Todos
const todoss = [
{
id: 1,
text: "First Todo"
},
{
id: 2,
text: "Second Todo"
},
{
id: 3,
text: "Third Todo"
},
{
id: 4,
text: "Fourth Todo"
}
]
export default todoss;
I then have my main page "Todolist.js", I have imported the data with "import TodoData from './TodoData'" at the top but I can't figure out exactly how to take that data and map it out onto the page, how would i do this?
You can use map() function to iterate an array.
import TodoData from './TodoData'
render() {
return (
<div>
{TodoData.map(function(data, idx){
return (<li key={idx}>{data.id}:{data.text}</li>)
})}
</div>
);
}
This is the output:
1:First Todo
2:Second Todo
3:Third Todo
4:Fourth Todo
You can use any styling you need.
Saving data internally as state is the "React" way of handling data.
In a real world application this data is going to come from an external source and if the developer doesn't know how to save data internally he will have no idea what to do.
components-and-props
state
Don't import the data, save it in the state of your Todos component and pass it as props to Todolist.
// this will act as a presentation of our data
const TodosList = ({ todos }) => (
<ul>
{todos.map(({ id, text }) => (
<li key={id}>{text}</li>
))}
</ul>
);
// This will act as a container for our data
class Todos extends React.Component {
state = {
todos: [
{
id: 1,
text: "First Todo"
},
{
id: 2,
text: "Second Todo"
},
{
id: 3,
text: "Third Todo"
},
{
id: 4,
text: "Fourth Todo"
}
]
};
render() {
return <TodosList todos={this.state.todos} />;
}
}