react functional component with ag grid cannot call parent function via context - reactjs

I am using ag-grid-react and ag-grid-community version 22.1.1. My app is written using functional components and hooks. I have a cellrenderer component that is attempting to call a handler within the parent component using the example found here. Is this a bug in ag-grid? I have been working on this application for over a year as I learn React, and this is my last major blocker so any help or a place to go to get that help would be greatly appreciated.
Cell Renderer Component
import React from 'react';
import Button from '../../button/button';
const RowButtonRenderer = props => {
const editClickHandler = (props) => {
let d = props.data;
console.log(d);
props.context.foo({d});
//props.editClicked(props);
}
const deleteClickHandler = (props) => {
props.deleteClicked(props);
}
return (<span>
<Button classname={'rowbuttons'} onClick={() => { editClickHandler(props) }} caption={'Edit'} />
<Button classname={'rowbuttons'} onClick={() => { deleteClickHandler(props) }} caption={'Delete'} />
</span>);
};
export default RowButtonRenderer;
Parent Component
function Checking() {
function foo(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent: (props) => foo(props)};
const gridOptions = (params) => {
if (params.node.rowIndex % 2 === 0) {
return { background: "#ACC0C6" };
}
};
const frameworkComponents = {
rowButtonRenderer: RowButtonRenderer,
};
.
.
.
return (
<>
<AgGridReact
getRowStyle={gridOptions}
frameworkComponents={frameworkComponents}
context = {context}
columnDefs={columnDefinitions}
rowData={rowData}
headerHeight="50"
rowClass="gridFont"
></AgGridReact>
</>
);
}
When clicking the edit button on a row, the debugger says that there is a function.
This error is received though:

You are passing the context object in this code section:
const context = {componentParent: (props) => foo(props)};
...
<AgGridReact
context={context}
{...}
></AgGridReact>
And in your cell renderer you call this
props.context.foo({d});
While it should be this
props.context.componentParent({d});
Also you can assign your callback directly since it receives the same parameter and returns the same result (if any)
function foo(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent: foo};
You can also use this shorthand syntax from ES6 when assigning object property
function componentParent(props) {
let toggle = displayModal
setNewData(props);
setModalDisplay(!toggle);
}
const context = {componentParent};
Live Demo

Related

REACT: How to set the state in the child and access it in the parent, receiving undefined

I am building this project to try and improve my understanding of react :), so I am a n00b and therefore still learning the ropes of extracting components, states, props etc =)
I have a child Component DescriptionDiv, its parent component is PlusContent and finally the parent component is PlusContentHolder. The user types some input into the DescriptionDiv which then, using a props/callback passes the user input to the PlusContent.
My question/problem is: after setting useState() in the PlusContent component, I am after a button click in the PlusContentHolder component, returned with an undefined in the console.log.
How come I cannot read the useState() in the next parent component, the PlusContentHolder?
I know that useState() is async so you cannot straight up call the value of the state in the PlusContent component, but shouldn't the state value be available in the PlusContentHolder component?
below is my code for the DescriptionDiv
import './DescriptionDiv.css';
const DescriptionDiv = props => {
const onDescriptionChangeHandler = (event) => {
props.descriptionPointer(event.target.value);
}
return (
<div className='description'>
<label>
<p>Description:</p>
<input onChange={onDescriptionChangeHandler} type='text'></input>
</label>
</div>);
}
export default DescriptionDiv;
Next the code for the PlusContent comp
import React, { useState } from "react";
import DescriptionDiv from "./div/DescriptionDiv";
import ImgDiv from "./div/ImgDiv";
import "./PlusContent.css";
import OrientationDiv from "./div/OrientationDiv";
const PlusContent = (props) => {
const [classes, setClasses] = useState("half");
const [content, setContent] = useState();
const [plusContent, setPlusContent] = useState({
orientation: "left",
img: "",
description: "",
});
const onOrientationChangeHandler = (orientationContent) => {
if (orientationContent == "left") {
setClasses("half left");
}
if (orientationContent == "right") {
setClasses("half right");
}
if (orientationContent == "center") {
setClasses("half center");
}
props.orientationInfo(orientationContent);
};
const onDescriptionContentHandler = (descriptionContent) => {
props.descriptionInfo(setPlusContent(descriptionContent));
console.log(descriptionContent)
};
const onImageChangeHandler = (imageContent) => {
props.imageInfo(imageContent);
setContent(
<>
<OrientationDiv
orientationPointer={onOrientationChangeHandler}
orientationName={props.orientationName}
/> {/*
<AltDiv altPointer={onAltDivContentHandler} />
<TitleDiv titlePointer={onTitleDivContentHandler} /> */}
<DescriptionDiv descriptionPointer={onDescriptionContentHandler} />
</>
);
};
return (
<div className={classes}>
<ImgDiv imageChangeExecutor={onImageChangeHandler} />
{content}
</div>
);
};
export default PlusContent;
and lastly the PlusContentHolder
import PlusContent from "../PlusContent";
import React, { useState } from "react";
const PlusContentHolder = (props) => {
const onClickHandler = (t) => {
t.preventDefault();
descriptionInfoHandler();
};
const descriptionInfoHandler = (x) => {
console.log(x) // this console.log(x) returns and undefined
};
return (
<div>
{props.contentAmountPointer.map((content) => (
<PlusContent
orientationInfo={orientationInfoHandler}
imageInfo={imageInfoHandler}
descriptionInfo={descriptionInfoHandler}
key={content}
orientationName={content}
/>
))}
<button onClick={onClickHandler}>Generate Plus Content</button>
</div>
);
};
export default PlusContentHolder;
The reason why the descriptionInfoHandler() function call prints undefined in its console.log() statement when you click the button, is because you never provide an argument to it when you call it from the onClickHandler function.
I think that it will print the description when you type it, however. And I believe the problem is that you need to save the state in the PlusContentHolder module as well.
I would probably add a const [content, setContent] = useState() in the PlusContentHolder component, and make sure to call setContent(x) in the descriptionInfoHandler function in PlusContentHolder.
Otherwise, the state will not be present in the PlusContentHolder component when you click the button.
You need to only maintain a single state in the PlusContentHolder for orientation.
Here's a sample implementation of your use case
import React, { useState } from 'react';
const PlusContentHolder = () => {
const [orientatation, setOrientation] = useState('');
const orientationInfoHandler = (x) => {
setOrientation(x);
};
const generateOrientation = () => {
console.log('orientatation', orientatation);
};
return (
<>
<PlusContent orientationInfo={orientationInfoHandler} />
<button onClick={generateOrientation}>generate</button>
</>
);
};
const PlusContent = ({ orientationInfo }) => {
const onDescriptionContentHandler = (value) => {
// your custom implementation here,
orientationInfo(value);
};
return <DescriptionDiv descriptionPointer={onDescriptionContentHandler} />;
};
const DescriptionDiv = ({ descriptionPointer }) => {
const handleChange = (e) => {
descriptionPointer(e.target.value);
};
return <input type="text" onChange={handleChange} />;
};
I would suggest to maintain the orientation in redux so that its easier to update from the application.
SetState functions do not return anything. In the code below, you're passing undefined to props.descriptionInfo
const onDescriptionContentHandler = (descriptionContent) => {
props.descriptionInfo(setPlusContent(descriptionContent));
};
This shows a misunderstanding of the use of state. Make sure you're reading about "lifting state" in the docs.
You're also declaring needless functions, e.g. onDescriptionContentHandler in your PlusContent. The PlusContent component could just pass the descriptionInfoHandler from PlusContentHolder prop directly down to DescriptionDiv, since onDescriptionContentHandler doesn't do anything except invoke descriptionInfoHandler.
You may want to consider restructuring your app so plusContent state is maintained in PlusContentHolder, and pass that state down as props. That state would get updated when DescriptionDiv invokes descriptionInfoHandler. It'd subsequently pass the updated state down as props to PlusContent.
See my suggested flowchart.

Observe (get sized) control (listen to events) over a nested component in the react and typescript application via the forwardRef function

I have a functional component called MyDivBlock
const MyDivBlock: FC<BoxProps> = ({ }) => {
{getting data...}
return (
<>
<div className='divBlock'>
{data.map((todo: { id: string; title: string }) =>
<div key={todo.id}>{todo.id} {todo.title} </div>)}
</div>
</>
);
};
I use it in such a way that MyDivBlock is nested as a child of
const App: NextPage = () => {
return (
<div>
<Box >
<MyDivBlock key="key0" areaText="DIV1" another="another"/>
</Box>
</div>
)
}
Note that MyDivBlock is nested in Box and MyDivBlock has no ref attribute. This is important because I need to write Box code with no additional requirements for my nested children. And anyone who will use my Box should not think about constraints and ref attributes.
Then I need to get the dimensions of MyDivBlock in the code of Box component, and later attach some event listeners to it, such as scrolling. These dimensions and listeners will be used in the Box component. I wanted to use Ref to control it. That is, the Box will later observe changes in the dimensions and events of MyDivBlock by creating a ref-reference to them
I know that this kind of parent-child relationship architecture is implemented through forwardRef
And here is the Box code:
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
export interface BoxProps extends React.ComponentProps<any> {
children?: Element[];
className: string;
}
export const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const methods = {
show() {
if (childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, function (item) {
console.log(item)})
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
return <div ref={childRef}> {children} </div>
});
ChildWithForwardRef.displayName = 'ChildWithForwardRef';
return (
<div
className={'BoxArea'}>
<button name="ChildComp" onClick={() => childRef.current.show()}>get Width</button>
<ChildWithForwardRef ref={childRef} />
</div>
);
}
export default Box;
The result of pressing the button:
childRef.current is present...
[...]
$$typeof: Symbol(react.element) key: "key0" props: {areaText: 'DIV1', another: 'another'}
[...] Object
offsetWidth = undefined
As you can see from the output, the component is visible through the created ref. I can even make several nested ones and get the same for all of them.
But the problem is that I don't have access to the offsetWidth and other properties.
The other challenge is how can I add the addEventListener?
Because it works in pure Javascript with their objects like Element, Document, Window or any other object that supports events, and I have ReactChildren objects.
Plus I'm using NextJS and TypeScript.
Didn't dive too deep into the problem, but this may be because you are passing the same childRef to both div inside ChildWithForwardRef and to ChildWithForwardRef itself. The latter overwrites the former, so you have the method .show from useImperativeHandle available but not offsetWidth. A quick fix is to rewrite ChildWithForwardRef to use its own ref:
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const ref = useRef<HTMLDivElement>()
const methods = {
show() {
if (ref.current) {
console.log("ref.current is present...");
React.Children.forEach(children, (item) => console.log(item))
console.log("offsetWidth = " + ref.current.offsetWidth);
} else {
console.log("ref.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
// Here ref instead of childRef
return <div ref={ref}> {children} </div>
});
But really I don't quite get why you would need ChildWithForwardRef at all. The code is basically equivalent to this simpler version:
const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const showWidth = () => {
if(childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, item => console.log(item))
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
}
return (
<div className={'BoxArea'}>
<button name="ChildComp" onClick={showWidth}>get Width</button>
<div ref={childRef}>{children}</div>
</div>
);
}
You can't solve this completely with React. I solved it by wrapping the child component, making it take the form of the parent.

React Context value gets updated, but component doesn't re-render

This Codesandbox only has mobile styles as of now
I currently have a list of items being rendered based on their status.
Goal: When the user clicks on a nav button inside the modal, it updates the status type in context. Another component called SuggestionList consumes the context via useContext and renders out the items that are set to the new status.
Problem: The value in context is definitely being updated, but the SuggestionList component consuming the context is not re-rendering with a new list of items based on the status from context.
This seems to be a common problem:
Does new React Context API trigger re-renders?
React Context api - Consumer Does Not re-render after context changed
Component not re rendering when value from useContext is updated
I've tried a lot of suggestions from different posts, but I just cannot figure out why my SuggestionList component is not re-rendering upon value change in context. I'm hoping someone can give me some insight.
Context.js
// CONTEXT.JS
import { useState, createContext } from 'react';
export const RenderTypeContext = createContext();
export const RenderTypeProvider = ({ children }) => {
const [type, setType] = useState('suggestion');
const renderControls = {
type,
setType,
};
console.log(type); // logs out the new value, but does not cause a re-render in the SuggestionList component
return (
<RenderTypeContext.Provider value={renderControls}>
{children}
</RenderTypeContext.Provider>
);
};
SuggestionPage.jsx
// SuggestionPage.jsx
export const SuggestionsPage = () => {
return (
<>
<Header />
<FeedbackBar />
<RenderTypeProvider>
<SuggestionList />
</RenderTypeProvider>
</>
);
};
SuggestionList.jsx
// SuggestionList.jsx
import { RenderTypeContext } from '../../../../components/MobileModal/context';
export const SuggestionList = () => {
const retrievedRequests = useContext(RequestsContext);
const renderType = useContext(RenderTypeContext);
const { type } = renderType;
const renderedRequests = retrievedRequests.filter((req) => req.status === type);
return (
<main className={styles.container}>
{!renderedRequests.length && <EmptySuggestion />}
{renderedRequests.length &&
renderedRequests.map((request) => (
<Suggestion request={request} key={request.title} />
))}
</main>
);
};
Button.jsx
// Button.jsx
import { RenderTypeContext } from './context';
export const Button = ({ handleClick, activeButton, index, title }) => {
const tabRef = useRef();
const renderType = useContext(RenderTypeContext);
const { setType } = renderType;
useEffect(() => {
if (index === 0) {
tabRef.current.focus();
}
}, [index]);
return (
<button
className={`${styles.buttons} ${
activeButton === index && styles.activeButton
}`}
onClick={() => {
setType('planned');
handleClick(index);
}}
ref={index === 0 ? tabRef : null}
tabIndex="0"
>
{title}
</button>
);
};
Thanks
After a good night's rest, I finally solved it. It's amazing what you can miss when you're tired.
I didn't realize that I was placing the same provider as a child of itself. Once I removed the child provider, which was nested within itself, and raised the "parent" provider up the tree a little bit, everything started working.
So the issue wasn't that the component consuming the context wasn't updating, it was that my placement of providers was conflicting with each other. I lost track of my component tree. Dumb mistake.
The moral of the story, being tired can make you not see solutions. Get rest.

Set React Context inside function-only component

My goal is very simple. I am just looking to set my react context from within a reusable function-only (stateless?) react component.
When this reusable function gets called it will set the context (state inside) to values i provide. The problem is of course you can't import react inside a function-only component and hence I cannot set the context throughout my app.
There's nothing really to show its a simple problem.
But just in case:
<button onCLick={() => PlaySong()}></button>
export function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //cannot call useContext in this component
}
If i use a regular react component, i cannot call this function onClick:
export default function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //fine
}
But:
<button onCLick={() => <PlaySong />}></button> //not an executable function
One solution: I know i can easily solve this problem by simply creating a Playbtn component and place that in every song so it plays the song. The problem with this approach is that i am using a react-player library so i cannot place a Playbtn component in there...
You're so close! You just need to define the callback inside the function component.
export const PlaySongButton = ({...props}) => {
const {setCurrentSong} = useContext(StoreContext);
const playSong = () => {
setCurrentSong("some song");
}
return (
<button
{...props}
onClick={() => playSong()}
/>
)
}
If you want greater re-usability, you can create custom hooks to consume your context. Of course where you use these still has to follow the rules of hooks.
export const useSetCurrentSong = (song) => {
const {setCurrentSong} = useContext(StoreContext);
setCurrentSong(song);
}
It is possible to trigger a hook function by rendering a component, but you cannot call a component like you are trying to do.
const PlaySong = () => {
const {setCurrentSong} = useContext(StoreContext);
useEffect( () => {
setCurrentSong("some song");
}, []
}
return null;
}
const MyComponent = () => {
const [shouldPlay, setShouldPlay] = useState(false);
return (
<>
<button onClick={() => setShouldPlay(true)}>Play</button>
{shouldPlay && <PlaySong />}
</>
)
}

UseEffect causes infinite loop with swipeable routes

I am using the https://www.npmjs.com/package/react-swipeable-routes library to set up some swipeable views in my React app.
I have a custom context that contains a dynamic list of views that need to be rendered as children of the swipeable router, and I have added two buttons for a 'next' and 'previous' view for desktop users.
Now I am stuck on how to get the next and previous item from the array of modules.
I thought to fix it with a custom context and custom hook, but when using that I am getting stuck in an infinite loop.
My custom hook:
import { useContext } from 'react';
import { RootContext } from '../context/root-context';
const useShow = () => {
const [state, setState] = useContext(RootContext);
const setModules = (modules) => {
setState((currentState) => ({
...currentState,
modules,
}));
};
const setActiveModule = (currentModule) => {
// here is the magic. we get the currentModule, so we know which module is visible on the screen
// with this info, we can determine what the previous and next modules are
const index = state.modules.findIndex((module) => module.id === currentModule.id);
// if we are on first item, then there is no previous
let previous = index - 1;
if (previous < 0) {
previous = 0;
}
// if we are on last item, then there is no next
let next = index + 1;
if (next > state.modules.length - 1) {
next = state.modules.length - 1;
}
// update the state. this will trigger every component listening to the previous and next values
setState((currentState) => ({
...currentState,
previous: state.modules[previous].id,
next: state.modules[next].id,
}));
};
return {
modules: state.modules,
setActiveModule,
setModules,
previous: state.previous,
next: state.next,
};
};
export default useShow;
My custom context:
import React, { useState } from 'react';
export const RootContext = React.createContext([{}, () => {}]);
export default (props) => {
const [state, setState] = useState({});
return (
<RootContext.Provider value={[state, setState]}>
{props.children}
</RootContext.Provider>
);
};
and here the part where it goes wrong, in my Content.js
import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
import SwipeableRoutes from 'react-swipeable-routes';
import useShow from '../../hooks/useShow';
import NavButton from '../NavButton';
// for this demo we just have one single module component
// when we have real data, there will be a VoteModule and CommentModule at least
// there are 2 important object given to the props; module and match
// module comes from us, match comes from swipeable views library
const ModuleComponent = ({ module, match }) => {
// we need this function from the custom hook
const { setActiveModule } = useShow();
// if this view is active (match.type === 'full') then we tell the show hook that
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match]);
return (
<div style={{ height: 300, backgroundColor: module.title }}>{module.title}</div>
);
};
const Content = () => {
const { modules, previousModule, nextModule } = useShow();
// this is a safety measure, to make sure we don't start rendering stuff when there are no modules yet
if (!modules) {
return <div>Loading...</div>;
}
// this determines which component needs to be rendered for each module
// when we have real data we will switch on module.type or something similar
const getComponentForModule = (module) => {
// this is needed to get both the module and match objects inside the component
// the module object is provided by us and the match object comes from swipeable routes
const ModuleComponentWithProps = (props) => (
<ModuleComponent module={module} {...props} />
);
return ModuleComponentWithProps;
};
// this renders all the modules
// because we return early if there are no modules, we can be sure that here the modules array is always existing
const renderModules = () => (
modules.map((module) => (
<Route
path={`/${module.id}`}
key={module.id}
component={getComponentForModule(module)}
defaultParams={module}
/>
))
);
return (
<div className="content">
<div>
<SwipeableRoutes>
{renderModules()}
</SwipeableRoutes>
<NavButton type="previous" to={previousModule} />
<NavButton type="next" to={nextModule} />
</div>
</div>
);
};
export default Content;
For sake of completion, also my NavButton.js :
import React from 'react';
import { NavLink } from 'react-router-dom';
const NavButton = ({ type, to }) => {
const iconClassName = ['fa'];
if (type === 'next') {
iconClassName.push('fa-arrow-right');
} else {
iconClassName.push('fa-arrow-left');
}
return (
<div className="">
<NavLink className="nav-link-button" to={`/${to}`}>
<i className={iconClassName.join(' ')} />
</NavLink>
</div>
);
};
export default NavButton;
In Content.js there is this part:
// if this view is active (match.type === 'full') then we tell the show hook that
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match]);
which is causing the infinite loop. If I comment out the setActiveModule call, then the infinite loop is gone, but of course then I also won't have the desired outcome.
I am sure I am doing something wrong in either the usage of useEffect and/or the custom hook I have created, but I just can't figure out what it is.
Any help is much appreciated
I think it's the problem with the way you are using the component in the Route.
Try using:
<Route
path={`/${module.id}`}
key={module.id}
component={() => getComponentForModule(module)}
defaultParams={module}
/>
EDIT:
I have a feeling that it's because of your HOC.
Can you try
component={ModuleComponent}
defaultParams={module}
And get the module from the match object.
const ModuleComponent = ({ match }) => {
const {type, module} = match;
const { setActiveModule } = useShow();
useEffect(() => {
if (type === 'full') {
setActiveModule(module);
}
},[module, setActiveModule]);
match is an object and evaluated in the useEffect will always cause the code to be executed. Track match.type instead. Also you need to track the module there. If that's an object, you'll need to wrap it in a deep compare hook: https://github.com/kentcdodds/use-deep-compare-effect
useEffect(() => {
if (match.type === 'full') {
setActiveModule(module);
}
},[match.type, module]);

Resources