passing object using context and doing iteration with map - reactjs

This is a simple question but I couldn't reach the final result after a lot of attempts. The problem is that I want to pass an object in context and use it in another file. And then do an iteration and create a specific element for each value.
App.jsx
const [activities, setActivity] = useState([
{
key: Math.random() * Math.random(),
name: 'Hello',
}
]);
const inputValue = useRef(null);
const addActivity = () => {
const activity = {
key: Math.random() * Math.random(),
name: inputValue.current.value,
};
setActivity(activities.concat(activity));
};
const value = {
// I want to pass this parameter - only activities has problem (Activity.jsx <h1>)
// I can't achieve activities.name in Activity.jsx
activities: [...activities],
functions: {
addActivity: addActivity
},
ref: {
inputValue: inputValue
}
};
<Context.Provider
value={value}
>
Context.js
export const Context = createContext();
Activity.jsx
const { activities, functions, ref } = useContext(Context);
return (
<section className="activity-container">
<input type="text" ref={ref.inputValue} />
<button onClick={functions.addActivity}>add!</button>
{
activities.map(activity => (
<h1>activity.name</h1>
))
}
</section>
);

I believe this is what you want:
// Sharing data through context
Context file:
// Context.js
import React, { useState, useRef, createContext } from "react";
export const DataContext = createContext();
const getRandom = () => Math.random() * Math.random();
const defaultValue = {
key: getRandom(),
name: "Hello"
};
const ContextProvider = ({ children }) => {
const [activities, setActivity] = useState([defaultValue]);
const inputValue = useRef(null);
const addActivity = () => {
const activity = {
key: getRandom(),
name: inputValue.current.value
};
setActivity([...activities, activity]);
};
const value = {
activities: [...activities],
functions: { addActivity },
ref: { inputValue }
};
return <DataContext.Provider value={value}>{children}</DataContext.Provider>;
};
export default ContextProvider;
Hook to read from context:
// useDataContext
import { useContext } from "react";
import { DataContext } from "./Context";
const useDataContext = () => {
const contextValue = useContext(DataContext);
return contextValue;
};
export default useDataContext;
Child Element where you want to receive the value from context:
// Child.js
import React from "react";
import useDataContext from "./useDataContext";
const Child = () => {
const data = useDataContext();
return (
<>
{data.activities.map((val, idx) => (
<div key={idx}>Name is {val.name}</div>
))}
</>
);
};
export default Child;
And the App container:
// App.js
import Child from "./Child";
import ContextProvider from "./Context";
export default function App() {
return (
<div className="App">
<ContextProvider>
<Child />
</ContextProvider>
</div>
);
}
I've created a sandbox for you to test.

You should make sure that the Activity.jsx component is wrapped with context provider, to get the proper value from the context.
I tried in this codesandbox, and it's working properly. You can refer to this and check what you are missing.

Related

Type errors when extending component more than one level using forwardRef and useImperativeHandle

I'm experimenting with extending components in React. I'm trying to extend Handsontable using forwardRef and useImperativeHandle. First I wrap Handsontable in my own BaseTable component, adding some methods. Then I extend the BaseTable in a CustomersTable component in the same way to add even more methods and behavior. Everything seems to work well until I try to consume the CustomersTable in CustomersTableConsumer where I get some type errors. The component works just fine, it's just Typescript that isn't happy.
BaseTable:
export type BaseTableProps = {
findReplace: (v: string, rv: string) => void;
} & HotTable;
export const BaseTable = forwardRef<BaseTableProps, HotTableProps>(
(props, ref) => {
const hotRef = useRef<HotTable>(null);
const findReplace = (value: string, replaceValue: string) => {
const hot = hotRef?.current?.__hotInstance;
// ...
};
useImperativeHandle(
ref,
() =>
({
...hotRef?.current,
findReplace
} as BaseTableProps)
);
const gridSettings: Handsontable.GridSettings = {
autoColumnSize: true,
colHeaders: true,
...props.settings
};
return (
<div>
<HotTable
{...props}
ref={hotRef}
settings={gridSettings}
/>
</div>
);
}
);
CustomersTable:
export type CustomerTableProps = HotTable & {
customerTableFunc: () => void;
};
export const CustomersTable = forwardRef<CustomerTableProps, BaseTableProps>(
(props, ref) => {
const baseTableRef = useRef<BaseTableProps>(null);
const customerTableFunc = () => {
console.log("customerTableFunc");
};
useImperativeHandle(
ref,
() =>
({
...baseTableRef?.current,
customerTableFunc
} as CustomerTableProps)
);
useEffect(() => {
const y: Handsontable.ColumnSettings[] = [
{
title: "firstName",
type: "text",
wordWrap: false
},
{
title: "lastName",
type: "text",
wordWrap: false
}
];
baseTableRef?.current?.__hotInstance?.updateSettings({
columns: y
});
}, []);
return <BaseTable {...props} ref={baseTableRef} />;
}
);
CustomerTableConsumer:
export const CustomerTableConsumer = () => {
const [gridData, setGridData] = useState<string[][]>([]);
const customersTableRef = useRef<CustomerTableProps>(null);
const init = async () => {
const z = [];
z.push(["James", "Richard"]);
z.push(["Michael", "Irwin"]);
z.push(["Solomon", "Beck"]);
setGridData(z);
customersTableRef?.current?.__hotInstance?.updateData(z);
customersTableRef?.current?.customerTableFunc();
customersTableRef?.current?.findReplace("x", "y"); };
useEffect(() => {
init();
}, []);
// can't access extended props from handsontable on CustomersTable
return <CustomersTable data={gridData} ref={customersTableRef} />;
};
Here is a Codesandbox example.
How do I need to update my typings to satisfy Typescript in this scenario?
You need to specify the type of the ref for forwardRef. This type is used then later in useRef<>().
It's confusing, because HotTable is used in useRef<HotTable>(), but BaseTable can't be used the same way, as it is a functional component and because forwardRef was used in BaseTable. So, basically, for forwardRef we define a new type and then later use that in useRef<>(). Note the distinction between BaseTableRef and BaseTableProps.
Simplified example
export type MyTableRef = {
findReplace: (v: string, rv: string) => void;
};
export type MyTableProps = { width: number; height: number };
export const MyTable = forwardRef<MyTableRef, MyTableProps>(...);
// then use it in useRef
const myTableRef = useRef<MyTableRef>(null);
<MyTable width={10} height={20} ref={myTableRef} />
Final solution
https://codesandbox.io/s/hopeful-shape-h5lvw7?file=/src/BaseTable.tsx
BaseTable:
import HotTable, { HotTableProps } from "#handsontable/react";
import { registerAllModules } from "handsontable/registry";
import { forwardRef, useImperativeHandle, useRef } from "react";
import Handsontable from "handsontable";
export type BaseTableRef = {
findReplace: (v: string, rv: string) => void;
} & HotTable;
export type BaseTableProps = HotTableProps;
export const BaseTable = forwardRef<BaseTableRef, BaseTableProps>(
(props, ref) => {
registerAllModules();
const hotRef = useRef<HotTable>(null);
const findReplace = (value: string, replaceValue: string) => {
const hot = hotRef?.current?.__hotInstance;
// ...
};
useImperativeHandle(
ref,
() =>
({
...hotRef?.current,
findReplace
} as BaseTableRef)
);
const gridSettings: Handsontable.GridSettings = {
autoColumnSize: true,
colHeaders: true,
...props.settings
};
return (
<div>
<HotTable
{...props}
ref={hotRef}
settings={gridSettings}
licenseKey="non-commercial-and-evaluation"
/>
</div>
);
}
);
CustomersTable:
import Handsontable from "handsontable";
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef
} from "react";
import { BaseTable, BaseTableRef, BaseTableProps } from "./BaseTable";
export type CustomerTableRef = {
customerTableFunc: () => void;
} & BaseTableRef;
export type CustomerTableProps = BaseTableProps;
export const CustomersTable = forwardRef<CustomerTableRef, CustomerTableProps>(
(props, ref) => {
const baseTableRef = useRef<BaseTableRef>(null);
const customerTableFunc = () => {
console.log("customerTableFunc");
};
useImperativeHandle(
ref,
() =>
({
...baseTableRef?.current,
customerTableFunc
} as CustomerTableRef)
);
useEffect(() => {
const y: Handsontable.ColumnSettings[] = [
{
title: "firstName",
type: "text",
wordWrap: false
},
{
title: "lastName",
type: "text",
wordWrap: false
}
];
baseTableRef?.current?.__hotInstance?.updateSettings({
columns: y
});
}, []);
return <BaseTable {...props} ref={baseTableRef} />;
}
);
CustomerTableConsumer:
import { useEffect, useRef, useState } from "react";
import { CustomersTable, CustomerTableRef } from "./CustomerTable";
export const CustomerTableConsumer = () => {
const [gridData, setGridData] = useState<string[][]>([]);
const customersTableRef = useRef<CustomerTableRef>(null);
// Check console and seee that customerTableFunc from customersTable,
// findReplace from BaseTable and __hotInstance from Handsontable is available
console.log(customersTableRef?.current);
const init = async () => {
const z = [];
z.push(["James", "Richard"]);
z.push(["Michael", "Irwin"]);
z.push(["Solomon", "Beck"]);
setGridData(z);
customersTableRef?.current?.__hotInstance?.updateData(z);
customersTableRef?.current?.customerTableFunc();
};
useEffect(() => {
init();
}, []);
return <CustomersTable data={gridData} ref={customersTableRef} />;
};
In your sandbox example it's almost correct, just fix the props type for CustomersTable. I would recommend though to not use Props suffix for ref types, as it is very confusing.
https://codesandbox.io/s/unruffled-framework-1xmltj?file=/src/CustomerTable.tsx
export const CustomersTable = forwardRef<CustomerTableProps, HotTableProps>(...)

How to pass props if it already has some value passed?

function UpdatePngf(index, id) {
const [Cards, setCards] = props.value.Cards
let CardsData = Cards
var CardsObj = {
link: Cards[index].link,
icon: Cards[index].icon,
name: Cards[index].name,
png: Cards[index].png,
id: id,
}
CardsData[index] = CardsObj
setCards(CardsData)
}
export const UpdatePng = withUserData(UpdatePngf)
this is my function I want to pass props..but how I am supposed to do so??
should I do this way function UpdatePngf(index, id,props) {}? or other way
/** #format */
import React, { createContext } from 'react'
const UserData = createContext(null)
export const withUserData = (Component) => (props) => {
return (
<UserData.Consumer>
{(value) => <Component {...props} value={value}></Component>}
</UserData.Consumer>
)
}
export default UserData
This is my userData hoc..
I saw index, id also is props. You just update UpdatePngf:
function UpdatePngf({index, id, ...props}) { ... }
And pass props to UpdatePng wwhen using it: <UpdatePng id="...." index="..." ...yourProps>

mobx-react-lite useObserver hook outside of render

I've seen examples of the useObserver hook that look like this:
const Test = () => {
const store = useContext(storeContext);
return useObserver(() => (
<div>
<div>{store.num}</div>
</div>
))
}
But the following works too, and I'd like to know if there's any reason not to use useObserver to return a value that will be used in render rather than to return the render.
const Test = () => {
const store = useContext(storeContext);
var num = useObserver(function (){
return store.num;
});
return (
<div>
<div>{num}</div>
</div>
)
}
Also, I don't get any errors using useObserver twice in the same component. Any problems with something like this?
const Test = () => {
const store = useContext(storeContext);
var num = useObserver(function (){
return store.num;
});
return useObserver(() => (
<div>
<div>{num}</div>
<div>{store.num2}</div>
</div>
))
}
You can use observer method in the component. And use any store you want.
import { observer } from "mobx-react-lite";
import { useStore } from "../../stores/StoreContext";
const Test = observer(() => {
const { myStore } = useStore();
return() => (
<div>
<div>{myStore.num}</div>
<div>{myStore.num2}</div>
</div>
)
}
);
StoreContext.ts
import myStore from './myStore'
export class RootStore{
//Define your stores here. also import them in the imports
myStore = newMyStore(this)
}
export const rootStore = new RootStore();
const StoreContext = React.createContext(rootStore);
export const useStore = () => React.useContext(StoreContext);

How to update custom hook value

I've started learning typescript and react hooks on my new web project. I have some problem with managing initial value of my custom hook i've made. Cant step over this functionality. How should I update nameInput hook, after data is fetched from my API. For example if I pass custom string as initial value in useTextInput everything is fine, but problem is when im passing undefined first there(when data is fetched from API) and value after is downloaded, and seems like it is not updating.
My question is, how to update in AccountInput inputValue value properly by value coming from API as userAccount.firstName from getUser() method, witch depends to nameInput, witch is not updated after initial value in useTextInput.
Account.tsx
import React, { useEffect, useState} from 'react';
import { connect } from 'react-redux';
import { IApplicationState } from '../../store';
import { actionCreators, reducer, AccountStatusEnum } from '../../store/Account';
import { MDBBtn, MDBInput } from 'mdbreact';
import { AccountInput } from './child-components';
import { useTextInput } from '../../hooks';
type AccountProps = ReturnType<typeof reducer> & typeof actionCreators;
const Account: React.FC<AccountProps> = ({ getUser, resetState, userAccount, status }) => {
const nameInput = useTextInput(userAccount.firstName);
const [isInputInvalid, setIsInputInvalid] = useState<boolean>(false);
useEffect(() => {
getUser();
}, []);
return (
<React.Fragment>
<div className="mt-3 container border border-light">
<h5 className="secondary-heading font-weight-bold pt-3 pl-5">User info</h5>
<div className="row">
<AccountInput
textInput={nameInput}
typeInput="text"
labelInput="Your name & surename"
isInputInvalid={isInputInvalid}
/>
</div>
</div>
</React.Fragment>
);
};
const mapStateToProps = (state: IApplicationState) => state.acc;
export default connect(mapStateToProps, actionCreators)(Account as any);
AccointInput.tsx
import React, { Fragment, useEffect, useState } from 'react';
import { TextInput } from '../../../hooks';
import { MDBInput } from 'mdbreact';
type AccountInputProps = {
readonly textInput: TextInput;
readonly isInputInvalid: boolean;
readonly typeInput: string;
readonly labelInput: string;
};
const AccountInput = React.memo<AccountInputProps>(({ textInput, isInputInvalid, typeInput, labelInput }) => {
const { hasValue, bindToInput } = textInput;
return (
<Fragment>
<div className="col-sm w-100 px-5">
<MDBInput hint={textInput.value} {...bindToInput} type={typeInput} label={labelInput} />
</div>
</Fragment>
);
});
AccountInput.displayName = 'AccountInput';
export default AccountInput;
useTextInput.ts
import { useState, useCallback, useMemo, FormEvent } from 'react';
export type TextInputType = 'text' | 'password';
export type TextInput = {
value: string;
hasValue: boolean;
clear: () => void;
bindToInput: {
value: string;
type: TextInputType;
onChange: (e: FormEvent<HTMLInputElement>) => void;
};
};
export const useTextInput = (initial: string = '', type: TextInputType = 'text'): TextInput => {
const [value, setValue] = useState<string>(initial);
const clear = useCallback((): void => setValue(''), []);
const onChange = useCallback((e: FormEvent<HTMLInputElement>): void => setValue(e.currentTarget.value), []);
return useMemo<TextInput>(
() => ({
value,
clear,
hasValue: !!(value && value.trim()),
bindToInput: {
type,
value,
onChange,
},
}),
[value, type, onChange, clear],
);
};
I don't know for sure how custom hooks handle promises, but normally when you want to pass an async value to useState you do something like this:
const useTextInput = (initial) => {
const [value, setValue] = useState(initial);
useEffect(() => {
setValue(initial);
}, [initial]);
};
You can use useEffect to update your stateful value when initial changes. Would this work in your case?

On child component state change, update siblings

Partially working example: https://codesandbox.io/s/jolly-smoke-ryb2d
Problem:
When a user expands/opens a component row, all other rows inside the rows parent component need to be collapsed. Unfortunately, I can't seem to get the other sibling rows to collapse.
I tried passing down a handler from the parent to the child that updates the state of the parent which would then in turn propagate down to the children.
Expected Result
On expand/open of a row, collapse any other rows that are open inside the parent component that isn't the one clicked
Code:
App.tsx
import React from "react";
import ReactDOM from "react-dom";
import Rows from "./Rows";
import Row from "./Row";
import "./styles.css";
export interface AppProps {}
const App: React.FC<AppProps> = props => {
return (
<Rows>
<Row>
<p>Click me</p>
<p>Collapse</p>
</Row>
<Row>
<p>Click me</p>
<p>Collapse</p>
</Row>
<Row>
<p>Click me</p>
<p>Collapse</p>
</Row>
</Rows>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Rows.tsx
Rows.tsx
import React, { useState, useEffect } from "react";
import Row, { RowProps } from "./Row";
export interface RowsProps {}
const Rows: React.FC<RowsProps> = props => {
const [areRowsHidden, setAreRowsHidden] = useState<boolean>(false);
useEffect(() => {});
const handleOnShow = (): void => {};
const handleOnCollapse = (): void => {};
const renderChildren = (): React.ReactElement[] => {
return React.Children.map(props.children, child => {
const props = Object.assign(
{},
(child as React.ReactElement<RowsProps>).props,
{
onShow: handleOnShow,
onCollapse: handleOnCollapse,
isCollapsed: areRowsHidden
}
);
return React.createElement(Row, props);
});
};
return <>{renderChildren()}</>;
};
export default Rows;
Row.tsx
import React, { useState, useEffect } from "react";
export interface RowProps {
onCollapse?: Function;
onShow?: Function;
isCollapsed?: boolean;
}
const Row: React.FC<RowProps> = props => {
const [isCollapsed, setIsCollapsed] = useState(props.isCollapsed || true);
useEffect(() => {}, [props.isCollapsed]);
const handleClick = (): void => {
if (isCollapsed) {
props.onShow();
setIsCollapsed(false);
} else {
props.onCollapse();
setIsCollapsed(true);
}
};
return (
<>
{React.cloneElement(props.children[0], {
onClick: handleClick
})}
{isCollapsed ? null : React.cloneElement(props.children[1])}
</>
);
};
export default Row;
I would store which row is open inside of Rows.tsx and send that value down to its children rather than having the child control that state. You may see this being referred to as lifting state up. You can read more about it here.
Rows.tsx
import React, { useState } from 'react'
import Row from './Row'
export interface RowsProps {}
const Rows: React.FC<RowsProps> = props => {
const [visibleRowIndex, setVisibleRowIndex] = useState<number>(null)
const renderChildren = (): React.ReactElement[] => {
return React.Children.map(props.children, (child, index) => {
const props = Object.assign({}, (child as React.ReactElement<RowsProps>).props, {
onShow: () => setVisibleRowIndex(index),
onCollapse: () => setVisibleRowIndex(null),
isCollapsed: index !== visibleRowIndex
})
return React.createElement(Row, props)
})
}
return <>{renderChildren()}</>
}
export default Rows
Row.tsx
import React from 'react'
export interface RowProps {
onCollapse?: Function
onShow?: Function
isCollapsed?: boolean
}
const Row: React.FC<RowProps> = props => {
const handleClick = (): void => {
if (props.isCollapsed) {
props.onShow()
} else {
props.onCollapse()
}
}
return (
<>
{React.cloneElement(props.children[0], {
onClick: handleClick
})}
{props.isCollapsed ? null : React.cloneElement(props.children[1])}
</>
)
}
export default Row
Example: https://codesandbox.io/s/gifted-hermann-oz2zw
Just a side note: I noticed you're cloning elements and doing something commonly referred to as prop drilling. You can avoid this by using context if you're interested although not necessary.

Resources