React hooks- how to set options in dropdown with a variable - reactjs

I'm working in a project where I'm trying to set the options of a dropdown with an array of objects that I'm getting from a request to an API.
The thing is that when I'm setting the options (key, value, text) with a map of that variable, it appears an error. So I think that way is not correct to what I'm doing.
Can you help me to know what to do in this case?
Here is my code:
import { Dropdown } from 'semantic-ui-react';
type formProps = {
funcionCierre: any
carrera: any;
nombre1: any;
}
const Estudiantes: React.FC<formProps> = (props: formProps) => {
const [area, setArea] = useState<any[]>([]);
useEffect(() => {
console.log(props.carrera);
axios.get('http://localhost:8003/skill?carrera_id=' + props.carrera + '&tipo_id=1')
.then(result => {
console.log(result);
setArea(result.data); //here is where i'm capturing my array of options
console.log(area);
}
).catch(error => {
console.log(error);
});
}, [area.length]);
return (
<Dropdown
placeholder='Area'
search
selection
options={area.map(ar => (
key: ar.skil_id, //here is where i'm trying to set the options
value: ar.skill_id,
text: ar.nombre
))}
/>)
Thanks in advance.

You are missing {} from your area.map(ar => (...)) call. It should be area.map(ar => ({...}))
const Dropdown = (props) => <div>{JSON.stringify(props.options)}</div>;
const Estudiantes = (props) => {
const [area, setArea] = React.useState([]);
React.useEffect(() => {
Promise.resolve([{skill_id: 1, nombre: 'one'}, {skill_id: 2, nombre: 'two'}])
.then(result => setArea(result))
}, [area.length]);
return (
<Dropdown
placeholder='Area'
options={area.map(ar => ({
key: ar.skil_id,
value: ar.skill_id,
text: ar.nombre
}))}
/>
)
}
ReactDOM.render(<Estudiantes />, document.getElementById("app"));
<script src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="app"></div>

Related

Updating Recursive Object state in Redux

I want to update the 'name' stored in child object of redux state.
Currently, I am using redux toolkit and storing, 'TElement' data (from api) in redux state.
TElement has recursice data structure.
I was able to map out all the child components in React. However, I don't know how to go about updating the state of TElement's elements.
createSlice.ts
export interface TElement {
id: string;
name: string;
link: string;
elements: TElement[];
};
const initalState: TElements = {
TElement: {
id: '',
name: '',
link: '',
elements: []
}
}
const systemSlice = createSlice({
name: 'system',
initialState: initialState as TElements,
reducers:{}
})
export const root = (state: RootState): TElements['TElement'] =>
state.system.TElement;
Component.tsx
'Wish to update name in input field'
const File: React.FC<TElement> = ({
id,
name,
link,
elements,
}: TElement) => {
const [showChildren, setShowChildren] = useState<boolean>(false);
const handleClick = useCallback(() => {
setShowChildren(!showChildren);
}, [showChildren, setShowChildren]);
return (
<div>
<input
onClick={handleClick}
style={{ fontWeight: showChildren ? 'bold' : 'normal' }}>
{name}
</input>
<div
style={{
position: 'relative',
display: 'flex',
flexDirection: 'column',
left: 25,
borderLeft: '1px solid',
paddingLeft: 15,
}}>
{showChildren &&
(child ?? []).map((node: FileNode) => <File key={id} {...node} />)}
</div>
</div>
)
function TaskFilter(): JSX.Element {
const root = useSelector(root);
return (
<div>
<File {...root} />
</div>
);
}
export default TaskFilter;
My recommendation would be to store them in a flat structure. This makes it more difficult to store them (if they are coming from the API in a nested structure), but much easier to update them.
You would store a dictionary of elements keyed by their id so that you can look up and update an element easily. You would replace the recursive element property with an array of the childIds of the direct children.
export interface TElement {
id: string;
name: string;
link: string;
elements: TElement[];
}
export type StoredElement = Omit<TElement, "elements"> & {
childIds: string[];
};
Here's what your slice might look like:
export const elementAdapter = createEntityAdapter<StoredElement>();
const flatten = (
element: TElement,
dictionary: Record<string, StoredElement> = {}
): Record<string, StoredElement> => {
const { elements, ...rest } = element;
dictionary[element.id] = { ...rest, childIds: elements.map((e) => e.id) };
elements.forEach((e) => flatten(e, dictionary));
return dictionary;
};
const systemSlice = createSlice({
name: "system",
initialState: elementAdapter.getInitialState({
rootId: "" // id of the root element
}),
reducers: {
receiveOne: (state, { payload }: PayloadAction<TElement>) => {
elementAdapter.upsertMany(state, flatten(payload));
},
receiveMany: (state, { payload }: PayloadAction<TElement[]>) => {
payload.forEach((element) =>
elementAdapter.upsertMany(state, flatten(element))
);
},
rename: (
state,
{ payload }: PayloadAction<Pick<TElement, "id" | "name">>
) => {
const { id, name } = payload;
elementAdapter.updateOne(state, { id, changes: { name } });
}
}
});
export const { receiveOne, receiveMany, rename } = systemSlice.actions;
export default systemSlice.reducer;
And the store:
const store = configureStore({
reducer: {
system: systemSlice.reducer
}
});
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
export const useSelector = createSelectorHook<RootState>();
const { selectById } = elementAdapter.getSelectors(
(state: RootState) => state.system
);
And your components:
const RenderFile: React.FC<StoredElement> = ({ id, name, link, childIds }) => {
const dispatch = useDispatch();
const [showChildren, setShowChildren] = useState(false);
const handleClick = useCallback(() => {
setShowChildren((prev) => !prev);
}, [setShowChildren]);
const [text, setText] = useState(name);
const onSubmitName = () => {
dispatch(rename({ id, name: text }));
};
return (
<div>
<div>
<label>
Name:
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
</label>
<button onClick={onSubmitName}>Submit</button>
</div>
<div>
<div onClick={handleClick}>
Click to {showChildren ? "Hide" : "Show"} Children
</div>
{showChildren && childIds.map((id) => <FileById key={id} id={id} />)}
</div>
</div>
);
};
const FileById: React.FC<{ id: string }> = ({ id }) => {
const file = useSelector((state) => selectById(state, id));
if (!file) {
return null;
}
return <RenderFile {...file} />;
};
const TaskFilter = () => {
const rootId = useSelector((state) => state.system.rootId);
return (
<div>
<FileById id={rootId} />
</div>
);
};
export default TaskFilter;
Code Sandbox Link
To understand recursion you have to understand recursion. Here is an example that will recursively render and in the action provide all the parent ids to the update so the reducer can recursively update.
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = {
elements: [
{
id: '1',
name: 'one',
elements: [
{
id: '2',
name: 'two',
elements: [
{
id: '3',
name: 'three',
elements: [],
},
],
},
],
},
{
id: '4',
name: 'four',
elements: [],
},
],
};
//action types
const NAME_CHANGED = 'NAME_CHANGED';
//action creators
const nameChanged = (parentIds, id, newName) => ({
type: NAME_CHANGED,
payload: { parentIds, id, newName },
});
//recursive update for reducer
const recursiveUpdate = (
elements,
parentIds,
id,
newName
) => {
const recur = (elements, parentIds, id, newName) => {
//if no more parent ids
if (parentIds.length === 0) {
return elements.map((element) =>
element.id === id
? { ...element, name: newName }
: element
);
}
const currentParent = parentIds[0];
//recursively update minus current parent id
return elements.map((element) =>
element.id === currentParent
? {
...element,
elements: recursiveUpdate(
element.elements,
parentIds.slice(1),
id,
newName
),
}
: element
);
};
return recur(elements, parentIds, id, newName);
};
const reducer = (state, { type, payload }) => {
if (type === NAME_CHANGED) {
const { parentIds, id, newName } = payload;
return {
...state,
elements: recursiveUpdate(
state.elements,
parentIds,
id,
newName
),
};
}
return state;
};
//selectors
const selectElements = (state) => state.elements;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
//Element will recursively call itself
const Element = React.memo(function ElementComponent({
parentIds,
element,
}) {
const dispatch = useDispatch();
const onNameChange = (e) =>
dispatch(
nameChanged(parentIds, element.id, e.target.value)
);
const { id } = element;
console.log('render', id);
//make parentIds array for children, use memo to not needlessly
// re render all elements on name change
const childParentIds = React.useMemo(
() => parentIds.concat(id),
[parentIds, id]
);
return (
<li>
<input
type="text"
value={element.name}
onChange={onNameChange}
/>
{/* SO does not support optional chaining but you can use
Boolean(element.elements?.length) instead */}
{Boolean(
element.elements && element.elements.length
) && (
<ul>
{element.elements.map((child) => (
// recursively render child elements
<Element
key={child.id}
element={child}
parentIds={childParentIds}
/>
))}
</ul>
)}
</li>
);
});
const App = () => {
const elements = useSelector(selectElements);
const parentIds = React.useMemo(() => [], []);
return (
<ul>
{elements.map((element) => (
<Element
key={element.id}
parentIds={parentIds}
element={element}
/>
))}
</ul>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>

Is it possible to call useCallback inside another callback

in component A:
const getOnClick = useCallback(
(rec: GenericRec): (() => void) => () => {
setSelectedRecord(rec);
},
[],
);
in componant B child of A :
const openRecord = useCallback(
(row: Row<Record>) => () => {
getOnClick({ id: row.original.id, name: row.original.name });
},
[getOnClick],
);
getOnClick is not called
You have an extra layer of functions in both of your useCallback calls. Unlike useMemo, useCallback directly accepts the callback that it should memoize, not a function that builds the callback.
So:
const getOnClick = useCallback(
(rec: GenericRec): (() => void) => {
// ^−−−− no () => here
console.log("getOnClick");
setSelectedRecord(rec);
},
[],
);
const openRecord = useCallback(
(row: Row<Record>) =>
// ^−−−− no () => here
getOnClick({ id: row.original.id, name: row.original.name })
,
[getOnClick],
);
Live Example (with TypeScript type annotations commented out):
const { useState, useCallback } = React;
function Example() {
const [selectedRecord, setSelectedRecord] = useState(null);
const getOnClick = useCallback(
(rec/*: GenericRec*/)/*: (() => void)*/ => {
console.log("getOnClick");
setSelectedRecord(rec);
},
[]/*,*/
);
const openRecord = useCallback(
(row/*: Row<Record>*/) =>
// ^−−−− no () => here
getOnClick({ id: row.original.id, name: row.original.name })
,
[getOnClick]/*,*/
);
const addRecord =() => {
console.log("addRecord");
openRecord({
original: {
id: 1,
name: "The record"
}
});
};
return (
<div>
<input type="button" value="Click Me" onClick={addRecord} disabled={!!selectedRecord} />
{selectedRecord && <span>Selected record: "{selectedRecord.name}" ({selectedRecord.id})</span>}
{!selectedRecord && <em>No record selected</em>}
</div>
);
}
ReactDOM.render(<Example/>, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

How to add a function in const Target = props => {

How do I add a function to connect to one of my components onChange? Creating a function like this returns an error code of 'cardActionResponse' is not defined.
What the benefit of using a const class like this?
const Target = props => {
const { markAsDone } = useContext(ItemContext);
const [{ isOver }, drop] = useDrop({
accept: 'Item',
drop: (item, monitor) => console.log(item),
collect: monitor => ({
isOver: !!monitor.isOver()
})
})
//Cannot do this. How else can I make a function to connect to CreateVideoCard?
cardActionResponse = (event) => {
console.log(event);
}
return (
<div className="target top80 right30" ref={drop} style={{ backgroundColor: isOver ? 'black' : '' }} >
<TitleDescription class="z1"/>
<div class="right10 left10">
<CreateVideoCard onChange={this.cardActionResponse} />
<CreateDescriptionCard></CreateDescriptionCard>
<CreateAudioCard></CreateAudioCard>
<CreateTermsCard></CreateTermsCard>
</div>
</div>
);
};
export default Target;
Functional components don't have it's own context (this), so you should simply use const variable.
Please use
const cardActionResponse = (event) => {
console.log(event);
}
and then
<CreateVideoCard onChange={cardActionResponse} />

Multiple useEffect and setState causing callback to be called twice

I'm test driving a pattern I found online known as meiosis as an alternative to Redux using event streams. The concept is simple, the state is produced as a stream of update functions using the scan method to evaluate the function against the current state and return the new state. It works great in all of my test cases but when I use it with react every action is called twice. You can see the entire app and reproduce the issue at CodeSandbox.
import state$, { actions } from "./meiosis";
const App = () => {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState({
title: "",
status: "PENDING"
});
useEffect(() => {
state$
.pipe(
map(state => {
return state.get("todos")
}),
distinctUntilChanged(),
map(state => state.toJS())
)
.subscribe(state => setTodos(state));
}, []);
useEffect(() => {
state$
.pipe(
map(state => state.get("todo")),
distinctUntilChanged(),
map(state => state.toJS())
)
.subscribe(state => setNewTodo(state));
}, []);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
{genList(todos)}
<div className="formGroup">
<input
type="text"
value={newTodo.title}
onChange={evt => actions.typeNewTodoTitle(evt.target.value)}
/>
<button
onClick = {() => {
actions.addTodo()
}}
>
Add TODO
</button>
<button
onClick={() => {
actions.undo();
}}
>UNDO</button>
</div>
</header>
</div>
);
};
Meisos
import { List, Record } from "immutable";
import { Subject } from "rxjs";
const model = {
initial: {
todo: Record({
title: "",
status: "PENDING"
})(),
todos: List([Record({ title: "Learn Meiosis", status: "PENDING" })()])
},
actions(update) {
return {
addTodo: (title, status = "PENDING") => {
update.next(state => {
console.log(title);
if (!title) {
title = state.get("todo").get("title");
}
const todo = Record({ title, status })();
return state.set("todos", state.get("todos").push(todo));
});
},
typeNewTodoTitle: (title, status = "PENDING") => {
update.next(state => {
return state.set("todo", Record({ title, status })())
});
},
resetTodo: () => {
update.next(state =>
state.set("todo", Record({ title: "", status: "PENDING" })())
);
},
removeTodo: i => {
update.next(state => state.set("todos", state.get("todos").remove(i)));
}
};
}
}
const update$ = new BehaviorSubject(state => state) // identity function to produce initial state
export const actions = model.actions(update$);
export default update$;
Solve my problem. It stemmed from a misunderstanding of how RXJS was working. An issue on the RxJS github page gave me the answer. Each subscriptions causes the observable pipeline to be re-evaluated. By adding the share operator to the pipeline it resolves this behavior.
export default update$.pipe(
scan(
(state, updater) =>
updater(state),
Record(initial)()
),
share()
);

How to set checked on item in DetailsList control

I use DetailsList component from office-ui-fabric-react library:
import {DetailsList} from 'office-ui-fabric-react/lib/DetailsList';
render () {
const item = [
{value: 'one'},
{value: 'two'}
]
return (
<DetailsList
checkboxVisibility={CheckboxVisibility.always}
items={items}
selection={selection}
/>
}
How to set checked for item with value `two?
Noticed you passed a selection to DetailsList. There's a few methods in selection to do that, including:
setAllSelected(isAllSelected: boolean)
setKeySelected(key: string, isSelected: boolean, shouldAnchor: boolean)
setIndexSelected(index: number, isSelected: boolean, shouldAnchor: boolean)
In your case, you can give each value a key. And then call setKeySelected somewhere (for example, componentDidMount) to tell DetailsList to select specific items.
I looked everywhere and could never find the answer. To set selected items basically on page load do the following:
<DetailsList
columns={columns}
items={getItems()}
selection={getSelectedItems()}
/>
const getItems = () => {
const items: any[] = [];
itemList.map((item: any, i) => {
items.push({
key: i.toString(),
Name: item.Name,
Description: item.Description
});
});
return [];
};
const getSelectedItems = (): ISelection => {
const selection: Selection = new Selection();
const items = getItems();
selection.setItems(items);
selectedItemsList.map((selectedItem:
any, s) => {
selection.setKeySelected(s.toString(), true, false);
});
return selection;
};
Simply call selection.setAllSelected(true) inside useEffect if you want all the list items to be selected by default.
const selection = new Selection();
useEffect(() => {
selection.setAllSelected(true)
}, [selection]);
React Typescript (tsx) Sample.
import { DetailsList, ISelection, Selection } from '#fluentui/react/lib/DetailsList';
// ...
export const Component = () => {
const getSelectionDetails = (): string => {
const selectionCount = selection.getSelection().length;
return selectionCount
? `${selectionCount} items selected: ${selection.getSelection().map( (i: any) => i.key ).join('; ')}`
: 'No items selected';
}
const createSelection = () => {
return new Selection({
onSelectionChanged: () => setSelectionDetails(getSelectionDetails()),
getKey: (item: any) => item.key
});
};
const [selection, setSelection] = React.useState(createSelection());
const [selectionDetails, setSelectionDetails] = React.useState('');
return <>
<DetailsList
// ...
selection={selection}
selectionMode={SelectionMode.multiple}
// ...
/>
</>;
}

Resources