Expose state and method of child Component in parent with React - reactjs

I know it's not a good pattern to do that, but you will understand why I want to do like that.
I have a HTable, which use a third-party library (react-table)
const HTable = <T extends object>({ columns, data, tableInstance}: Props<T>) {
const instance: TableInstance<T> = useTable<T> (
// Parameters
)
React.useImperativeHandle(tableInstance, () => instance);
}
Now, I want to control columns visibility from parent. I did:
const Parent = () => {
const [tableInstance, setTableInstance] = React.useState<TableInstance<SaleItem>>();
<Table data={data} columns={columns} tableInstance={(instance) => setTableInstance(instance)}
return tableInstance.columns.map((column) => {
<Toggle active={column.isVisible} onClick={() =>column.toggleHiden()}
}
}
The column hides well, but the state doesn't update and neither does the toggle, and I don't understand why. Could you help me to understand?
EDIT:
Adding a sandbox.
https://codesandbox.io/s/react-table-imperative-ref-forked-dilx3?file=/src/App.js
Please note that I cannot use React.forwardRef, because I use typescript and React.forwardRef doesn't allow generic type like this if I use forwardRef
interface TableProps<T extends object> {
data: T[],
columns: Column<T>[],
tableInstance?: React.RefObject<TableInstance<T>>,
}

Your issue is that react-tables useTable() hook always returns the same object as instance wrapper (the ref never changes). So your parent, is re-setting tableInstance to the same object - which does not trigger an update. Actually most of the contained values are also memoized. To get it reactive grab the headerGroups property.
const {
headerGroups,
...otherProperties,
} = instance;
React.useImperativeHandle(
tableInstance,
() => ({ ...properties }), // select properties individually
[headerGroups, ...properties],
);

Related

How can I make transient/one-off state changes to a React component from one of its ancestors?

I want to modify the state of a child component in React from a parent component a couple levels above it.
The child component is a react-table with pagination.
My use case is changing the data in the table with some client-side JS filtering.
The problem is, the table uses internal state to keep track of which page is being shown, and does not fully update in response to my filtering.
It is smart enough to know how much data it contains, but not smart enough to update the page it is on.
So, it might correctly say "Showing items 21-30 of 85", and then the user filters the data down to only four total items, and the table will say "Showing items 21-30 of 4".
I tried implementing something like what the FAQ suggests for manual state control, but that caused its own problem.
I was passing the new page index in as a prop, and that did set the page correctly, but it broke the ability for the user to navigate between pages, because any changes were immediately overwritten by the value of the prop.
Those instructions seem to work for a situation where all page index control gets handled by the parent, but not when some control should still be retained by the pagination mechanism.
I think what I need is an exposed function that lets me modify the value of the table's state.pageIndex as a one-off instead of passing a permanent prop. Is there a way to do that? Or any other way to solve my underlying problem?
Code follows. I apologize in advance I couldn't make this a real SSCCE, it was just too complicated, I tried to at least follow the spirit of SSCCEs as much as I could.
My page that lists stuff for the user looks like this:
// ...
const [searchTerms, setSearchTerms] = useState<Array<string>>([]);
// ...
const handleFilterRequestFromUser = function (searchTerms): void {
// ...
setSearchTerms(processedSearchTerms);
};
// ...
const visibleData = useMemo(() => {
// ...
}, [searchTerms]);
// ...
return (
<div>
// ...
<ImmediateParentOfTable
id={"Results"}
visibleData={visibleData} // User actions can affect the size of this
// ...
>
// ...
</div>
);
export default ListDatabaseResults;
Here's ImmediateParentOfTable:
import { Table, Pagination } from "#my-company/react";
// ...
return (
<Table
id={id}
pagination={{
render: (
dataSize,
{
pageCount,
pageOptions,
// ...
}
) => (
<Pagination
dataSize={dataSize}
pageCount={pageCount}
pageOptions={pageOptions}
gotoPage={gotoPage}
previousPage={previousPage}
nextPage={nextPage}
setPageSize={setPageSize}
canPreviousPage={canPreviousPage}
canNextPage={canNextPage}
pageIndex={pageIndex}
pageSize={pageSize}
pageSizeOptions={[10, 20, 50, 100]}
/>
),
manual: {
onPageChange: ({
pageIndex,
pageSize,
}: {
pageIndex: number;
pageSize: number;
}) => {
setPageIndex(pageIndex);
setPageSize(pageSize);
},
rowCount,
pageCount: tablePageCount,
},
isLoading: !!dataLoading,
}}
/>
);
The custom Table inside #my-company/react (already in use in other places, so, difficult to modify):
import {
CellProps,
Column,
Hooks,
Row,
SortingRule,
TableState,
useFlexLayout,
usePagination,
UsePaginationInstanceProps,
UsePaginationState,
useRowSelect,
useSortBy,
useTable,
} from 'react-table';
// ...
export interface TableProps<D extends Record<string, unknown>> {
id: string;
// ...
pagination?: Pagination<D>;
pageIndexOverride?: number; // This is the new prop I added that breaks pagination
}
const Table = <D extends Record<string, unknown>>({
id,
columns,
data,
// ...
pageIndexOverride,
}: TableProps<D>): JSX.Element => {
const {
state: { pageIndex, pageSize, sortBy },
// ...
} = useTable(
{
columns,
data,
autoResetPage,
initialState,
useControlledState: (state) => {
return React.useMemo(
() => ({
...state,
pageIndex: pageIndexOverride || state.pageIndex, // This always resets page index to the prop value, so changes from the pagination bar no longer work
}),
[state],
);
},
// ...
I've encountered a similar problem with react-table where most of my functionality (pagination, sorting, filtering) is done server-side and of course when a filter is changed I must set the pageIndex back to 0 to rectify the same problem you have mentioned.
Unfortunately, as you have discovered, controlled state in v7 of react-table is both poorly documented and apparently just completely non-functional.
I will note that the example code you linked from the docs
const [controlledPageIndex, setControlledPage] = React.useState(0)
useTable({
useControlledState: state => {
return React.useMemo(
() => ({
...state,
pageIndex: controlledPageIndex,
}),
[state, controlledPageIndex]
)
},
})
is actually invalid. controlledPageIndex cannot be used as a dep in that useMemo because it is in the outer scope and is accessed through closure. Mutating it will do nothing, which is actually noted by eslint react/exhaustive-deps rule so it's quite surprising that this made it into the docs as a way of accomplishing things. There are more reasons why it is unusable, but the point is that you can forget using useControlledState for anything.
My suggestion is to use the stateReducer table option and dispatch a custom action that will do what you need it to. The table reducer actions can have arbitrary payloads so you can do pretty much whatever you want. ajkl2533 in the github issues used this approach for row selection (https://github.com/TanStack/react-table/issues/3142#issuecomment-822482864)
const reducer = (newState, action) => {
if (action.type === 'deselectAllRows') {
return { ...newState, selectedRowIds: {} };
}
return newState;
}
...
const { dispatch, ... } = useTable({ stateReducer: reducer }, ...);
const handleDeselectAll = () => {
dispatch({ type: 'deselectAllRows' });
}
It will require getting access to the dispatch from the useTable hook though.

React ForwardRef: Property 'current' does not exist on type 'ForwardedRef<HTMLElement>'

I am trying to create a component that will track the vertical scroll. The catch is – the actual scroll container is not easily predictable (in this specific case it is neither window, document nor body – it is div#__next, due to CSS overflow rules).
I want to keep the component flexible and self-contained. So I've created a ref with DOM selector as an argument. I know it is far from idiomatic (to say the least), but it suprisingly seems to be working:
// Parent component
import { useRef } from "react"
const Article = (props) => {
const scrollContainerRef = useRef<HTMLElement | null>(
document.querySelector("#__next") // <-- the scroll container reference
)
return (
<SomeContent>
<ScrollToTop treshold={640} ref={scrollContainerRef} />
</SomeContent>
)
// ScrollToTop
const ScrollToTop = forwardRef(
({ treshold }, ref) => {
const [visible, setVisible] = useState(false)
useEffect(() => {
if (ref?.current) {
ref.current.addEventListener("scroll", throttle(toggleVisible, 300))
return () => {
ref.current.removeEventListener("scroll", throttle(toggleVisible, 300))
}
}
}, [])
// …
So what's the problem? the current one is Typescript. I've spent hours trying to get the types right, but to no avail. The parent component is red squigly lines free (unless I pass globalThis, which seems to work at least in CodeSandbox), but the ScrollToTop is compaining whenever I am accessing current property:
Property 'current' does not exist on type 'ForwardedRef<HTMLElement>'.
I've tried to use React.MutableRefObject<HTMLElement | null /* or other T's */>, both in parent and in child, but it didn't help.
Any ideas how to get the types to match? Or is this a silly idea from the beginning?
CodeSandbox demo
Refs might be objects with a .current property, but they might also be functions. So you can't assume that a forwarded ref has a .current property.
I think it's a mistake to use forwardRef at all here. The purpose of forwardRef is to allow a parent component to get access to an element in a child component. But instead, the parent is the one finding the element, and then you're passing it to the child for it to use. I would use a regular state and prop for that:
const Article = (props) => {
const [scrollContainer, setScrollContainer] = useState<HTMLElement | null>(() => {
return document.querySelector("#__next");
});
return (
<SomeContent>
<ScrollToTop treshold={640} scrollContainer={scrollContainer} />
</SomeContent>
)
interface ScrollToTopProps {
treshold: number;
scrollContainer: HTMLElement | null;
}
const ScrollToTop = ({ treshold, scrollContainer }: ScrollToTopProps) => {
const [visible, setVisible] = useState(false);
useEffect(() => {
if (scrollContainer) {
const toggle = throttle(toggleVisible, 300);
scrollContainer.addEventListener("scroll", toggle);
return () => {
scrollContainer.removeEventListener("scroll", toggle);
}
}
}, [scrollContainer]);
// ...
}

Create Dynamic Components

I want to dynamically create a component, when I implement something like this:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const App = () => {
const Comp = gen_Comp("Hello");
return (
<Comp txt="World" />
);
}
Something goes wrong (what exactly goes wrong is hard to explain because it's specific to my app, point is that I must be doing something wrong, because I seem to be losing state as my component gets rerendered). I also tried this with React.createElement, but the problem remains.
So, what is the proper way to create components at runtime?
The main way that react tells whether it needs to mount/unmount components is by the component type (the second way is keys). Every time App renders, you call gen_Comp and create a new type of component. It may have the same functionality as the previous one, but it's a new component and so react is forced to unmount the instance of the old component type and mount one of the new type.
You need to create your component types just once. If you can, i recommend you use your factory outside of rendering, so it runs just when the module loads:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const Comp = gen_Comp("Hello");
const App = () => {
return (
<Comp txt="World" />
);
}
If it absolutely needs to be done inside the rendering of a component (say, it depends on props), then you will need to memoize it:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const App = ({ spec }) => {
const Comp = useMemo(() => {
return gen_Comp(spec);
}, [spec]);
return (
<Comp txt="World" />
);
}

Knowing when a map state props update

I have the following app in react and redux start kid
in a component, I am using a series of selector that are related to the same store Items :
const mapStateToProps = (state: RootState) => ({
itemsLoading: ItemsSelectors.getItemsIsLoading(state),
items: ItemsSelectors.getCurrentItemList(state),
fields: ItemsSelectors.getCurrentItemFields(state),
columns: ItemsSelectors.getCurrentItemColumns(state)
})
When the store values changes, I would like to update my component state, by doing some calculation with the data.
I am using the following function
UNSAFE_componentWillUpdate(nextProps) {
const displaybleTable = this.getDisplaybleTable(nextProps);
this.setState({
items : displaybleTable.items,
columns : displaybleTable.columns
})
}
So everytime the store change, I get updated, and I update the component state.
The problem is, since I update the component state, I am looping in this function.
Also, I believe it looks a bit wierd.
IS there a way to know when the store value has updates in the component, so thatr component can do some personal data manipulation ?
Which version of react do you use?
If I understood you correctly and assuming react version 16.8+, you can achiev this by using the useEffect() hook. I assume your component is connected to the store using connect() from 'react-redux'. Then it could look like this:
const MyComponent = (props) => {
useEffect(() => {
const displaybleTable = this.getDisplaybleTable(/* arguments */);
this.setState({
items : displaybleTable.items,
columns : displaybleTable.columns
})
}, [props.items])
const getDisplayableTable = (/* args: any */) => {
return ...
}
...
}
export const MyConnectedComponent = connect(
(state: RootState) => ({
itemsLoading: ItemsSelectors.getItemsIsLoading(state),
items: ItemsSelectors.getCurrentItemList(state),
fields: ItemsSelectors.getCurrentItemFields(state),
columns: ItemsSelectors.getCurrentItemColumns(state)
}),
{
// dispatchProps ...
},
(stateProps: any, dispatchProps: any, ownProps: any) => ({
itemsLoading: stateProps.itemsLoading,
items: stateProps.items,
fields: stateProps.fields,
columns: stateProps.columns
})
)(MyComponent)
The second parameter of useEffect defines when useEffect() calls the first parameter, which is a function. So each time 'items' is updated in the store, the update will trigger useEffect which will run the code and sets the state of your component.
EDIT:
ComponentWillUpdate(nextProps) will not be called if some values in your store changes. ComponentWillUpdate only gets called if the props you pass to your component has changed:
export const SomeOtherComponent = (props: any) => {
return (
<MyComponent prop1={val1} prop2={val2} />
)
}
If val1 and val2 changes this would call ComponentWillUpdate of MyComponent (as far as I know, but I'm not sure).

React - Decorating Children and Handling Keys

I'm writing a small component that decorates some children. Given the following sample:
const Child = props => <div>{props.name}</div>;
const Parent = props => {
let wrappedChildren = React.Children.map(props.children, c => {
return (<Decorator key={c.key}>
{c}
</Decorator>);
});
return (<div>
{wrappedChildren}
</div>);
}
const Consumer = props => {
let children = [0, 1, 2].map(num => {
return <Child key={num} name={num} />;
});
return <Parent>{children}</Parent>;
});
In this code, I'm wanting to take each child and decorate it with some wrapping container or some behaviour. Forgetting for the moment that there may only be one child, I need to give each instance a key.
Currently I'm assuming that each child does have a key which isn't fantastic, lifting it off the child and applying it to the Decorator directly.
Is this the "correct" way of doing this? Is there a better way?
I think your approach is fine. And you do need the key on the top level. Use the child's key, if it is there. If not, fall back to the index, as React recommends:
When you don't have stable IDs for rendered items, you may use the item index as a key as a last resort.
Be advised though:
We don't recommend using indexes for keys if the items can reorder, as that would be slow.
Source: React Docs about keys
const Child = props => <div>{props.name}</div>;
const Parent = props => {
let wrappedChildren = React.Children.map(props.children, (c, i) => {
const key = c.key ? `key-${c.key}` : `index-${i}`
return (
<Decorator key={key}>
{c}
</Decorator>
);
});
return (
<div>
{wrappedChildren}
</div>
);
};
const Consumer = () => {
let children = [ 0, 1, 2 ].map(num => {
return <Child key={num} name={num} />;
});
return <Parent>{children}</Parent>;
};
That would work with the current version of React, 15.6.1 (and probably with prior versions as well). However, there is a slightly better way to achieve your goal with a small tweak, which would be delegating the lifting on a prop, rather than using directly the key.
The reason is that the concept of a key is something that is controlled by React internals before your component gets created, so it falls into the implementation details category, with the consequence that future versions of React could break your code without you even noticing it.
Instead, you can use a prop on your Child component, for instance id, based on your assumption that each child does have some sort of unique value. Your code would result in:
const Child = props => <div>{props.name}</div>;
const Parent = props => {
return React.Children.map(props.children, c => {
return (<Decorator key={c.props.id}>
{c}
</Decorator>);
});
return (<div>
{wrappedChildren}
</div>);
}
const Consumer = props => {
let children = [0, 1, 2].map(num => {
return <Child id={num} name={num} />;
});
return <Parent>{children}</Parent>;
});
If you have a more complex structure and want to avoid passing props individually, the parent component should held the responsibility of generating unique ids for the children. You can follow the ideas explained in this answer https://stackoverflow.com/a/29466744/4642844.
Those are the steps I'd follow:
Implement your own utility function or use an existing browser library to generate unique ids.
Implement componentWillMount in your Parent component to create an array of unique ids. You can use an internal member class variable instead of local state. It would be something like:
componentWillMount() {
this.uuids = React.Children.map(() => uuid());
}
Inside your Parent render, assign each key to the Decorator component in the following way.
render() {
return React.Children.map(props.children, (c, i) => {
return (<Decorator key={this.uuids[i]}>
{c}
</Decorator>);
});
}
Some useful links:
http://mxstbr.blog/2017/02/react-children-deepdive/
https://medium.com/#dan_abramov/react-components-elements-and-instances-90800811f8ca
Javascript Array.map(fn) passes actually 3 arguments to fn second being an index. So, what you need to do is:
let wrappedChildren = props.children.map((c, i) => <Decorator key={i}>{c}</Decorator>);

Resources