I have a simple document maker that allows users the create multiple fields of two types (input field and label field). They can also rearrange the field via drag and drop. My data structure is based on the Redux guide on data normalization.
LIST field is input type (e.g. children id 21 and 22) - onChange will dispatch action and allows the input text to modified without re-rendering other fields.
With LABEL field we allow users to select from a dropdown list of only 3 labels (Danger, Slippery, Flammable). Selecting one with remove it from the dropdown list to prevent duplicate label. For example, if there are "Danger" and "Slippery" in the field, the dropdown field will only show one option "Flammable". To achieve this, I use createSelector to get all children and filter them base on their parent (fieldId). This gives me an array of existing labels ["Danger","Slippery"] in that fieldId. This array will then be used to filter from a fixed dropdown array of three options with useEffect.
Now whenever I update the input text, it will also re-render the LABEL field (based on the Chrome React profiler).
It does not affect performance but I feel like I am missing something with my createSelector.
Example:
export const documentSlice = createSlice({
name: "document",
initialState: {
fields: {
1: { id: 1, children: [11, 12] },
2: { id: 2, children: [21, 22] },
},
children: {
11: { id: 11, type: "LABEL", text: "Danger", fieldId: 1 },
12: { id: 11, type: "LABEL", text: "Slippery", fieldId: 1 },
21: { id: 21, type: "LIST", text: "", fieldId: 2 },
22: { id: 22, type: "LIST", text: "", fieldId: 2 },
},
fieldOrder:[1,2]
},
});
createSelector
export const selectAllChildren = (state) => state.document.children;
export const selectFieldId = (state, fieldId) => fieldId;
export const getChildrenByFieldId = createSelector(
[selectAllChildren, selectFieldId],
(children, fieldId) => {
const filterObject = (obj, filter, filterValue) =>
Object.keys(obj).reduce(
(acc, val) =>
obj[val][filter] !== filterValue ? acc : [...acc, obj[val].text],
[]
);
const existingChildren = filterObject(children, "fieldId", filterId);
return existingChildren;
}
);
After more reading up, this is what finally works for me. Hopefully someone will find it useful.
Given the normalized data, the object reduce function can be simplified.
slice.js
export const getChildrenByFieldId = createSelector(
[selectAllChildren, selectFieldId],
(children, fieldId) => {
// get the children from fields entry => array [11,12]
const childrenIds = state.document.fields[fieldId].children
let existingChildrenText = [];
// loop through the created array [11,12]
childrenIds.forEach((childId) => {
// pull data from the individual children entry
const childText = children[childId].text;
existingChildrenText.push(childText);
});
return existingChildrenText;
}
);
To prevent re-render, we can use shallowEqual comparison on the output array to compare only the values of the array.
//app.js
import { shallowEqual, useSelector } from "react-redux";
const childrenTextData = useSelector((state) => {
return getChildrenByFieldId(state, blockId);
}, shallowEqual);
Related
I'm trying to create a table component that can display its cells flexibly. Every cells on a column displays the same cell component. The table component take an array of object as initial data and an array of column properties. Table should looks like this:
interface MyTableProps {
columns: ColumnProps[];
initialData: Array<any>; // Should array of object
callbackTableDataChanged: (newData) => void; // being used to send modified table data back to MyTable's father components.
}
interface ColumnProps {
label: string;
objectPropertyName: string; // (*)
renderCell: "cell-type-1" | "cell-type-2" | "cell-type-3" | ... // (**)
}
const MyTable: React.FC<MyTableProps> = tableProps => {
// The table should have its own a copy of initial data, because of many reasons
const [tableData, setTableData] = useState(tableProps.initialData);
const handleDataChange = (thePropertyName: string, rowIndex: number, newValue: any) => {
// This function changes the table data array,
// whenever the cell component (MyCellComp1, MyCellComp2, ...) on a column make a change of value.
// For example: MyCellComp1 is a time picker, whenever a new timestamp selected,
// it will send new time value through a callback, to its father component (aka MyTable).
// Then, this func will do the changes to the corresponding "place" in the data array object of this MyTable.
}
const renderRows = (column: ColumnProps, rowIndex: number) => {
switch (column.renderCell) {
case cell-type-1: return <CellComp1 ... onDataChanged={cellData => handleDataChange(column.objectPropertyName, rowIndex, cellData)}/>
case cell-type-2: return <CellComp2 ... onDataChanged={cellData => handleDataChange(column.objectPropertyName, rowIndex, cellData)}/>
...
}
}
return (
{renderColumnLabels}
<Flatlist data={tableData} renderItem={({col, index}) => renderRows(col, index)}/>
)
}
(*): "objectPropertyName" is a string that has value which is a name of one of the properties of initial object data. Forgive my awkward grammar!
(**): I use "renderCell" to tell the table what it should render at specific column. All cells on a column have the same component type.
The table should be used like this:
const initialData = [
{id: "123", name: "Tom"}, {id: "456", name: "Jerry"}, ...
]
const columns: ColumnProps[] = [
{label: "The ID", objectPropertyName: "id", renderCell: "cell-type-1"},
{label: "The name", objectPropertyName: "name", renderCell: "cell-type-2"},
...
];
<MyTable columns={columns} initialData={initialData} ... />
// This is how table looks like:
// column 1 column 2
// labels row: The ID The name
// row 1 : <CellComp1 /> <CellComp2 />
// row 2 : <CellComp1 /> <CellComp2 />
// ...
The problem is that with above approach, with the way I declared how cells on a column should be rendered, it limits the variety of cell component that MyTable could display. I declare 3 values for ColumnProps.rendercell, columns can display only 3 kinds of cell components.
My solution is that ColumnProps will have a new props which is a function that return a component:
interface ColumnProps {
label: string;
objectPropertyName: string; // (*)
renderCell: "cell-type-1" | "cell-type-2" | "cell-type-3" | ... // (**)
renderCellComponent?: () => JSX.Element
}
The new "renderCellComponent" prop will replace default cell components with whatever it returns.
However, with is method, I cant change the data array of MyTable with its function "handleDataChange".
For example:
const initialData = [
{id: "123", name: "Tom"}, {id: "456", name: "Jerry"}, ...
]
const columns: ColumnProps[] = [
{label: "The ID", objectPropertyName: "id", renderCell: "cell-type-1", renderCellComponent: () => <NewCellComp1 ... />},
{label: "The name", objectPropertyName: "name", renderCell: "cell-type-2", renderCellComponent: () => <NewCellComp2 ... />},
...
];
<MyTable columns={columns} initialData={initialData} ... />
As you can see, NewCellComp1 and NewCellComp2 need to access the function "handleDataChange" of MyTable just like the way CellComp1 and CellComp2 did. In order to do that, I tried to use React.forwardRef and useImperativeHandle to MyTable:
const MyTable: React.FC<MyTableProps> = React.forwardRef((tableProps, ref) => {
...
useImperativeHandle(ref, () => {
refFuncHandleDataChange(objectPropertyName: string, rowIndex, newValue) {
handleDataChange(objectPropertyName, rowIndex, newValue);
}
})
return ...
})
Now, MyTable should be used like this:
const initialData = [
{id: "123", name: "Tom"}, {id: "456", name: "Jerry"}, ...
]
const columns: ColumnProps[] = [
{...other props, objectPropertyName: "id", renderCellComponent: () => <NewCellComp1 onDataChanged={data => refTable.current?.refFuncHandleDataChange("id", ..., data)} />},
{...other props, objectPropertyName: "name", renderCellComponent: () => <NewCellComp2 onDataChanged={data => refTable.current?.refFuncHandleDataChange("name", ..., data)} />},
...
];
const refTable = useRef();
<MyTable ref={refTable} columns={columns} initialData={initialData} ... />
As you can see, with this approach, I still lack the row index of the cell component that make a change of data. When cell rendering is declared inside MyTable, I can access to row index easily thanks to Flatlist, but outside MyTable does not offer that luxury, because "renderCellComponent" function of ColumnProps is a declaration for all cells on a column. This is where I'm stuck.
In conclusion, I want to create a table component that be able to display every kind of components on its cells. Also, keep its properties as "simple" as possible. You can see that I'm currently must declare only data and columns props. But my approach seems to be impossible to do that.
Can you share me an improvement for what I did or an entirely new approach for this problem. Thank you!
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"]
}
));
}}
/>
);
}
Redux store cannot use immediately.
Backgound:
I have created a parent component and a child component.
The parent component will fetch a list of data, save to redux store,and generate child component.
example:
result:[
{
key: 1,
roomNo: '01',
status: 'OCCUPIED',
},
{
key: 2,
roomNo: '02',
status: 'UNOCCUPIED',
},
]
Then, there will be 2 child component (which is a button). And if I click the child button. The application will call DB again for more detail and save to redux store also.
detailResult: [
{
key: 1,
roomNo: '01',
status: 'OCCUPIED',
roomColor: 'red',
},
]
And since I would like to check whether the status is the same (result.roomNo="01"(OCCUPIED) and detailResult.roomNo="01"(OCCUPIED) ) (to ensure the data is the most updated one when user click the button)
Therefore, I write the following function in child component:
const { reduxStore } = useSelector((state) => ({
reduxStore : state.reduxStore ,
}));
const retrieve = useCallback(
(params) => {
dispatch(Actions.wardFetchRoom(params));
},
[dispatch]
);
const handleClick = (event) => {
const params = { roomNo: roomNo};
retrieve(params);
console.log('Step1', props.bedNo);
console.log('Step2', props.status);
console.log('Step3', reduxStore.room.status);
//function to match props.status === reduxStore.room.status
};
The problem comes when I click the the child button. The data always compare with previous store data but not current store data
T0: render finish
T1: click <child room="1">
T2: console: "Step1 = 1, Step2 = OCCUPIED, Step3 = NULL" (I expect "Step3 = OCCUPIED",since retrieve(params) is completed and the store should be updated at this moment)
T3: click <child room="2">
T4: console: "Step1 = 2, Step2 = UNOCCUPIED, Step3 = OCCUPIED" (This "Step3" data is come from T2, the data is delay for 1 user action)
Therefore, how can I change my code to use the store data immediately?
I relatively new to React and Semantic UI as well.
There is a component called Dropdown with a props multiple and selection, which allows to select multiple items.
On the output my state looks like this:
const selectedItems = [
{key: 1, value: 1, text: 'Item 1'},
{key: 2, value: 2, text: 'Item 2'},
{key: 3, value: 3, text: 'Item 3'},
];
How can I do setup limit of N amount of elements?
Many thanks
The Semantic UI React Dropdown option provides a function called as onAddItem. You will have to use the value data key and do something like this:
const onAddItem = (event, data) => {
1.Fetch the state of the selected values, stored in the value key
2. Check if the limit is greater than 2
3. If the condition is met, then add
4. Else, show an error
}
Documentation Link
Well according to https://react.semantic-ui.com/modules/dropdown#dropdown-example-multiple-selection you need to create controlled component, which means you will bind value={this.state.selectedItems} then you will bind onChange={(e,data) => { this.handleChange(e,data )} and in your code
onChange (e, data) {
const currentItems = this.state.selectedItems
if (currentItems.length <= MAX_SELECTION ) {
currentItems.push(data)
this.setState({
selectedItems: currentItems
})
}
}
this will crate controlled component which will allows you to control state by yourself, and you will limit changing state, probably you will need to also handle removing items from state inside this onChange event.
I would like to suggest another approach.
set useState directly to the dropdown value.
import React, { useState } from 'react';
import { Dropdown } from 'semantic-ui-react';
const MAX_FRUITS_SELECTION = 3;
const FruitsSelect = () => {
const [fruitId, setFruitId] = useState([]);
const optionsFRUITSFake = [
{ key: 1, value: 1, text: 'Orange' },
{ key: 2, value: 2, text: 'Lemon' },
{ key: 3, value: 3, text: 'Apple' },
{ key: 4, value: 4, text: 'Banana' },
{ key: 5, value: 5, text: 'Melon' },
{ key: 6, value: 6, text: 'Pineapple' }
];
const handleDropFilterFruit = (e: any, data?: any) => {
if (data.value.length <= MAX_FRUITS_SELECTION) {
setFruitId(data.value);
}
};
return (
<Dropdown
placeholder="Select Fruits"
onChange={handleDropFilterFruit}
value={fruitId}
fluid
multiple
selectOnNavigation={false}
search
selection
options={optionsFRUITSFake}
/>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<FruitsSelect />
</React.StrictMode>,
rootElement
);
<!DOCTYPE html>
<html lang="en">
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
</body>
</html>
I'm posting my workaround here. It's probably more short and simple.
At first, save the values in the state (preferably redux state) after every onChange. React state also would do fine. Then, make it disabled when a certain array length of the value is reached.
const districtData = ['Dhaka', 'Bagerhat', 'Bandarban',
'Barguna', 'Barishal', 'Bhola']
const [districtValue, setDistrictValue] = useState();
<Dropdown
onChange={async (e, { name, value }) => {setDistrictValue(value)}}
options={districtData.map((currentValue, index) => {
return {
key: `${currentValue}`,
value: `${currentValue}`,
text: `${currentValue}`,
disabled: districtValue.length > 2 ? true : false
}
// Do other things here
// Max 3 values here, then options will be disabled.
// Will be live again if any selected options are less than 3 here
/>
In my React state, I want to reorder an array of 3 objects by always putting the selected one in the middle while keeping the others in ascending order.
Right now, I'm using an order property in each object to keep track of the order, but this might not be the best approach.
For example :
this.state = {
selected: 'item1',
items: [
{
id: 'item1',
order: 2
},
{
id: 'item2'
order: 1
},
{
id: 'item3'
order: 3
}
]
}
Resulting array : [item2, item1, item3]
Now, let's imagine that a user selects item2. I will update the selected state property accordingly, but how can I update the items property to end up with a result like this:
this.state = {
selected: 'item2',
items: [
{
id: 'item1',
order: 1
},
{
id: 'item2'
order: 2
},
{
id: 'item3'
order: 3
}
]
}
Resulting array : [item1, item2, item3]
How would you do it? I have seen some lodash utility functions that could help but I would like to accomplish this in vanilla JavaScript.
You could do something crude like this:
// Create a local shallow copy of the state
var items = this.state.items.slice()
// Find the index of the selected item within the current items array.
var selectedItemName = this.state.selected;
function isSelectedItem(element, index, array) {
return element.id === selectedItemName;
};
var selectedIdx = items.findIndex(isSelectedItem);
// Extract that item
var selectedItem = items[selectedIdx];
// Delete the item from the items array
items.splice(selectedIdx, 1);
// Sort the items that are left over
items.sort(function(a, b) {
return a.id < b.id ? -1 : 1;
});
// Insert the selected item back into the array
items.splice(1, 0, selectedItem);
// Set the state to the new array
this.setState({items: items});
This assumes the size of the items array is always 3!
I'm gonna be lazy and just outline the steps you need to take.
Pop the selected item out of the starting array
Push the first item of the starting array into a new array
Push the selected item into the new array
Push the last item of the starting array into the new array
Set your state to use the new array
You can do something like:
NOTE: This works assuming there would three items in the array. However, if there are more we just need to specify the index position in the insert function.
this.state = {
selected: 'item1',
items: [
{
id: 'item1',
order: 1
},
{
id: 'item2',
order: 2
},
{
id: 'item3',
order: 3
}
]
};
// To avoid mutation.
const insert = (list, index, newListItem) => [
...list.slice(0, index), // part of array before index arg
newListItem,
...list.slice(index) // part of array after index arg
];
// Get selected item object.
const selectedValue = value => this.state.items.reduce((res, val) => {
if (val.id === selectedValue) {
res = val;
}
return res;
}, {});
const filtered = this.state.items.filter(i => i.id !== state.selected);
const result = insert(filtered, 1, selectedValue(this.state.selected));
We can get rid of the extra reduce if instead of storing id against selected you store either the index of the item or the whole object.
Of course we need to use this.setState({ items: result }). This solution would also ensure we are not mutating the original state array at any point.
I put together a fully working example what can be extended on so you can experiment with different ways to achieve your intended use-case.
In this case I created a button component and rendered three of them to provide a means of changing the selected state.
Important things to remember, always use the setState() function for updating React Class state. Also, always work on state arrays and objects with a cloned variable as you'll want to update the whole object/array at once. Don't modify attributes of pointer variables pointing to state objects or arrays.
It is very possible to add bugs to your code by referencing state objects/arrays and then changing their properties (accidentally or not) by modifying the pointer referencing the object. You will lose all guarantees on how the state will update, and comparing prevState or nextState with this.state may not work as intended.
/**
* #desc Sub-component that renders a button
* #returns {HTML} Button
*/
class ChangeStateButton extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.state = ({
//any needed state here
});
}
handleClick(e) {
//calls parent method with the clicked button element and click state
this.props.click(e.nativeEvent.toElement.id);
}
render() {
return (
<button
id = {this.props.id}
name = {this.props.name}
className = {this.props.className}
onClick = {this.handleClick} >
Reorder to {this.props.id}!
</button>
);
}
}
/**
* #desc Creates button components to control items order in state
* #returns {HTML} Bound buttons
*/
class ReorderArrayExample extends React.Component {
constructor(props) {
super(props);
this.reorderItems = this.reorderItems.bind(this);
this.state = ({
selected: 'item1',
//added to give option of where selected will insert
selectedIndexChoice: 1,
items: [
{
id: 'item1',
order: 2
},
{
id: 'item2',
order: 1
},
{
id: 'item3',
order: 3
}
]
});
}
reorderItems(selected) {
const {items, selectedIndexChoice} = this.state,
selectedObjectIndex = items.findIndex(el => el.id === selected);
let orderedItems = items.filter(el => el.id !== selected);
//You could make a faster reorder algo. This shows a working method.
orderedItems.sort((a,b) => { return a.order - b.order })
.splice(selectedIndexChoice, 0, items[selectedObjectIndex]);
//always update state with setState function.
this.setState({ selected, items: orderedItems });
//logging results to show that this is working
console.log('selected: ', selected);
console.log('Ordered Items: ', JSON.stringify(orderedItems));
}
render() {
//buttons added to show functionality
return (
<div>
<ChangeStateButton
id='item1'
name='state-button-1'
className='state-button'
click={this.reorderItems} />
<ChangeStateButton
id='item2'
name='state-button-2'
className='state-button'
click={this.reorderItems} />
<ChangeStateButton
id='item3'
name='state-button-2'
className='state-button'
click={this.reorderItems} />
</div>
);
}
}
/**
* #desc React Class renders full page. Would have more components in a real app.
* #returns {HTML} full app
*/
class App extends React.Component {
render() {
return (
<div className='pg'>
<ReorderArrayExample />
</div>
);
}
}
/**
* Render App to DOM
*/
/**
* #desc ReactDOM renders app to HTML root node
* #returns {DOM} full page
*/
ReactDOM.render(
<App/>, document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root">
<!-- This div's content will be managed by React. -->
</div>