alternative to innerRef when using hooks - javascript

I am converting class based components to react hooks. I got confused on using the ref parts. Because, the way I am using it complains me that The "innerRef" API has been removed in styled-components v4 in favor of React 16 ref forwarding, use "ref" instead like a typical component..
How do i make it work when using hooks?
const Tabs = ({activeTab, children}) => {
const [tabsElements, setTabsElements] = useState([])
return (
<TabsContext.TabProvider activeTab={activeTab}>
<TabsContext.TabConsumer>
{value => (
<ReactTabs>
<TabsContainer>
<ListTabs>
{value.context.tabs.map(tab => (
<TabTitleItem
key={tab.id}
onClick={value.context.onClick(tab)}
id={tab.id}
innerRef={tabElement => {
if (!tabsElements[tab.id]) {
setTabsElements(tabElements => ({
...tabElements,
[tab.id]: tabElement,
}))
}
}}
isActiveTab={value.context.activeTab.id === tab.id}
>
<TabAnchorItem>{tab.title}</TabAnchorItem>
</TabTitleItem>
))}
</ListTabs>
<ActiveTabBorder
activeTabElement={tabsElements[value.context.activeTab.id]}
/>
</TabsContainer>
{children}
</ReactTabs>
)}
</TabsContext.TabConsumer>
</TabsContext.TabProvider>
)
}
Here is the demo
https://codesandbox.io/s/z3moq8662p

First of all you cannot update state within the ref callback method. Secondly you simply need to pass ref instead of innerRef to the TabTitleItem component since it internally handles ref using forwardRef
const Tabs = ({ activeTab, children }) => {
const [tabsElements, setTabsElements] = useState([]);
const tabElements = useRef({});
return (
<TabsContext.TabProvider activeTab={activeTab}>
<TabsContext.TabConsumer>
{value => (
<ReactTabs>
<TabsContainer>
<ListTabs>
{console.log("value", value.context)}
{value.context.tabs.map(tab => (
<TabTitleItem
key={tab.id}
onClick={value.context.onClick(tab)}
id={tab.id}
ref={tabElement => {
tabElements.current[tab.id] = tabElement;
}}
isActiveTab={value.context.activeTab.id === tab.id}
>
<TabAnchorItem>{tab.title}</TabAnchorItem>
</TabTitleItem>
))}
</ListTabs>
<ActiveTabBorder
activeTabElement={tabsElements[value.context.activeTab.id]}
/>
</TabsContainer>
{children}
</ReactTabs>
)}
</TabsContext.TabConsumer>
</TabsContext.TabProvider>
);
};
Working demo

Related

React setting the state of all rendered items with .map in parent component

I have a parent component with a handler function:
const folderRef = useRef();
const handleCollapseAllFolders = () => {
folderRef.current.handleCloseAllFolders();
};
In the parent, I'm rendering multiple items (folders):
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
name={folder.name}
content={folder.content}
id={folder.id}
ref={folderRef}
/>
))}
In the child component I'm using the useImperativeHandle hook to be able to access the child function in the parent:
const [isFolderOpen, setIsFolderOpen] = useState(false);
// Collapse all
useImperativeHandle(ref, () => ({
handleCloseAllFolders: () => setIsFolderOpen(false),
}));
The problem is, when clicking the button in the parent, it only collapses the last opened folder and not all of them.
Clicking this:
<IconButton
onClick={handleCollapseAllFolders}
>
<UnfoldLessIcon />
</IconButton>
Only collapses the last opened folder.
When clicking the button, I want to set the state of ALL opened folders to false not just the last opened one.
Any way to solve this problem?
You could create a "multi-ref" - ref object that stores an array of every rendered Folder component. Then, just iterate over every element and call the closing function.
export default function App() {
const ref = useRef([]);
const content = data.map(({ id }, idx) => (
<Folder key={id} ref={(el) => (ref.current[idx] = el)} />
));
return (
<div className="App">
<button
onClick={() => {
ref.current.forEach((el) => el.handleClose());
}}
>
Close all
</button>
{content}
</div>
);
}
Codesandbox: https://codesandbox.io/s/magical-cray-9ylred?file=/src/App.js
For each map you generate new object, they do not seem to share state. Try using context
You are only updating the state in one child component. You need to lift up the state.
Additionally, using the useImperativeHandle hook is a bit unnecessary here. Instead, you can simply pass a handler function to the child component.
In the parent:
const [isAllOpen, setAllOpen] = useState(false);
return (
// ...
{folders &&
folders.map(folder => (
<CollapsableFolderListItem
key={folder.id}
isOpen={isAllOpen}
toggleAll={setAllOpen(!isAllOpen)}
// ...
/>
))}
)
In the child component:
const Child = ({ isOpen, toggleAll }) => {
const [isFolderOpen, setIsFolderOpen] = useState(false);
useEffect(() => {
setIsFolderOpen(isOpen);
}, [isOpen]);
return (
// ...
<IconButton
onClick={toggleAll}
>
<UnfoldLessIcon />
</IconButton>
)
}

React Parsing string as html and applying a function to DOM with forwardRef

On a ReactJs project I try to parse with com.wiris.js.JsPluginViewer.parseElement(ref, true, function () {}); function.
According to the docs, that function is to be applied to specific elements on the DOM. In the samples with pure javascript, they first set innerHTML of dom element and then apply the function.
So I thought that it would be handled using refs in React. (not sure if its the best way)
I have an array which includes some string to be rendered as html. I took all refs as an array with useRef([]) and then set each element's ref using refs.current[index]
According to the type I directly render string with dangerouslySetInnerHTML or use a wrapper component to render with a child component on which I would use that special function.
But I couldn't reach innerHTML property of the ref before applying the function in the WirisWrapper. I tried ref.innerHTML and ref.current.innerHTML
Parser as Parent
import { useRef, createRef } from 'react';
import WirisWrapper from './WirisWrapper';
const Parser = ({ itemsArray }) => {
let refs = useRef([]);
refs.current = itemsArray.map((ref, index) => (refs.current[index] = createRef()));
return (
<div>
{itemsArray.map((item, index) =>
item.type === 'math' ? (
<WirisWrapper
ref={el => (refs.current[index] = el)}
key={index}
mString={item.html}
/>
) : (
<div key={index} dangerouslySetInnerHTML={{ __html: item.html}}>
</div>
)
)}
</div>
);
};
export default Parser;
WirisWrapper as Child
import { forwardRef, useEffect } from 'react';
const WirisWrapper = forwardRef((props, ref) => {
const { mString } = props;
useEffect(() => {
if (ref.current && com.wiris.js.JsPluginViewer) {
ref.current.innerHTML = mString;
com.wiris.js.JsPluginViewer.parseElement(ref, true, function () {});
}
}, [ref, com.wiris.js.JsPluginViewer]);
return <div ref={ref}></div>;
});
export default WirisWrapper;
refs.current = itemsArray.map((ref, index) => (refs.current[index] = createRef())); looks to be creating a new React ref each render cycle and mutating the existing array at the same time.
You want to only create React refs if they don't previously exist. Map the itemsArray array to a new array each render, returning existing refs or creating new refs.
refs.current = itemsArray.map((ref, i) => refs.current[index] ?? createRef()));
Then just access the ref by index when mapping the UI.
{itemsArray.map((item, index) =>
item.type === 'math' ? (
<WirisWrapper
ref={refs.current[index]}
key={index}
mString={item.html}
/>
) : (
<div key={index} dangerouslySetInnerHTML={{ __html: item.html}}>
</div>
)
)}

Passing props inorder to use the string value

I have the following functional component and de-structuring the parameter props:
const MyTimes = ({ myWindowGroup, name, fieldArrayName }) => (
<FieldArray
name={name}
render={(arrayHelpers) => (
<React.Fragment>
{myWindowGroup.fieldArrayName.map((myTime, index) => (
and I am calling the component with the following props:
<MyTimes
myWindowGroup={myWindowGroup}
fieldArrayName={"myTimes"}
name={`myWindowGroups.${index}.myTimes`}
/>
My question is as I am new to React and that is, how can I pass/use the fieldArrayName={"myTimes"} string value of myTimes into the MyTime component above so that I can replace the value of fieldArrayName.map to be myTimes.map ?
I've tried it the way it is and not working.
Use dynamic keys. See Bracket Notation.
myWindowGroup[fieldArrayName].map(....
const myWindowGroup = {
myTimes: ['This is the value array you want'],
foo: [],
};
console.log(myWindowGroup['myTimes']);
console.log(myWindowGroup['foo']);
You can just do
{myWindowGroup[fieldArrayName].map((myTime, index) => (
As I understand your question correctly, you can achieve desired output by following
const MyTimes = ({ myWindowGroup, name, fieldArrayName }) => (
<FieldArray
name={name}
render={(arrayHelpers) => (
<React.Fragment>
{myWindowGroup[fieldArrayName].map((myTime, index) => (
// your logic here
))}

How to ref to multiple components inside map

I need to set ref to each project inside map function. I'm forwardingRef from child to my parent and currently I'm getting only single Project. I need to set refs to array list and then work with it. I tried something like this:
const projectSectionItem = useRef([])
ref={projectSectionItem => projectSectionItem[index] = projectSectionItem}
PARENT:
const projectSectionItem = useRef<HTMLDivElement>(null)
<StyledSection ref={projectSection}>
{data.map((i, index) => {
return i.showOnHP === 1 ?
<Element name={index.toString()} key={index}>
<Project
url={`${URL}${i.button_image.image}`}
client={i.client_name}
project={i.project.en}
btn_color={"#000"}
btn_text={i.button_text}
href={`/cases/${i.slug}`}
ref={projectSectionItem}
>
</Project>
</Element>
: null
})}
CHILD:
export const Project = React.forwardRef<HTMLDivElement, ProjectProps>((props, ref) => {
return (
<StyledProject className={props.className} ref={ref}>
...
</StyledProject>
)
})
Since you should not use hooks (useRef) in a loop or conditionally, you can just use createRef like this:
const pressureRefs = data.filter(item => item.showOnHP).map(() => React.createRef<HTMLDivElement>())
<Project ... ref={refs[i]} />

useMemo on function used in .map statement

I am pretty much new to hooks so have a question here:
I have in React component function like
const getSection = assignmentSectionId => {
const findSection = sections.find(
section => section.sectionRefId === assignmentSectionId,
);
return findSection ? findSection.name : '';
};
and now I got suggestion to use useMemo on that function. Currently I am using like:
return (
<List
role="list"
aria-label={ariaLabel}
>
{assignments.map(assignment => {
const sectionName = getSection(assignment.sectionId);
return (
<Card
name={sectionName}
/>
);
})}
</List>
);
};
What is the best(optimal) way to use useMemo here if it is possible?
You could use the Array#map inside the useMemo .Its only re-render the list After assignments value change
const memoList = React.useMemo(()=> assignments.map(assignment => {
const sectionName = getSection(assignment.sectionId);
return (<Card name={sectionName}/>)
}),[assignments])
return (
<List role="list" aria-label={ariaLabel}>{memoList}</List>
);
};

Categories