I have a dynamic array (state) of React components – and each components has an entry-animation on mount. But every time a component is added to the array all the components re-renders – which also triggers the entry-animation for all components...
My parent page looks something like this:
export default function Home({ projects }) {
const [components, setComponents] = useState([]);
const createComponent = (project) => {
const id = uniqid();
const temp = <Project block={project} key={id} />;
setOpenBlocks((prevState) => [temp, ...prevState]);
};
return (
<>
//Small images / Create component on click:
<div>
{projects.map((project, i) =>
<div key={project.page.id}>
<Image src alt
onClick={() => createComponent(project)}
/>
</div>
})}
</div>
//Big images / render array of components:
<div>
{components &&
components.map((block, i) => <Fragment key={i}>{component}</Fragment>)}
</div>
</>
);
}
And my 'Project' (child) component looks like this:
export default function Project({ project }) {
const [loaded, setLoaded] = useState(0);
return (
<AnimatePresence>
{project && (
<motion.figure
initial={{ width: 0 }}
animate={{ width: "100%" }}
style={{ opacity: loaded }}
>
<img
onLoad={() => setLoaded(1)}
/>
</motion.figure>
)}
</AnimatePresence>
)
}
So the entry-animation is made via the framer-motion AnimatePresence component, as well as the onLoad function changing opacity from 0 to 1. Both of them re-triggers when updating the array. And I need only the component that was just added to animate!
The child components props will not change once it is rendered.
I've tried wrapping the child component in 'memo', and tried updating the array with useCallback. Inserting the array like this somehow seemed to work (but I don't think it should?):
<div>
{components}
</div>
All input is welcome, thanks!
I have 2 different components AnimeList and MangaList that share similar logic, so I want to share code between them. Here is how I'm currently doing it:
const AnimeList = (props) => <AnimangaList isAnime={true} {...props} />;
const MangaList = (props) => <AnimangaList isAnime={false} {...props} />;
return (
<Tab.Navigator>
<Tab.Screen name="Anime" component={AnimeList} />
<Tab.Screen name="Manga" component={MangaList} />
</Tab.Navigator>
);
Is there a shorter or more convenient way to do this in React / React Native? ie. Something similar to function.bind()? This feels pretty hefty.
I usually go with static array when I need to have different values for few properties on large amount of components.
const pages = [
{
isAnime: true,
name: 'Anime',
},
{
isAnime: false,
name: 'Manga',
},
];
// mocks for some components
const MangaList = ({ isAnime }) => <div>{isAnime}</div>;
const Screen = ({ name, component: Component }) => (
<div>
{name}
<Component />
</div>
);
And then inside Tab.Navigator:
{pages.map((p, i) => (
<Screen key={i} name={p.name} component={(props) => <MangaList isAnime={p.isAnime} {...props} />} />
))}
My main functional component performs a huge amount of useQueries and useMutations on the child component hence I have set it as React.memo so as to not cause re-rendering on each update. Basically, when new products are selected I still see the old products because of memo.
mainFunction.js
const [active, setActive] = useState(false);
const handleToggle = () => setActive(false);
const handleSelection = (resources) => {
const idsFromResources = resources.selection.map((product) => product.variants.map(variant => variant.id));
store.set('bulk-ids', idsFromResources); //loal storage js-store library
handleToggle
};
const emptyState = !store.get('bulk-ids'); // Checks local storage using js-store library
return (
<Page>
<TitleBar
title="Products"
primaryAction={{
content: 'Select products',
onAction: () => {
setActive(!active)
}
}}
/>
<ResourcePicker
resourceType="Product"
showVariants={true}
open={active}
onSelection={(resources) => handleSelection(resources)}
onCancel={handleToggle}
/>
<Button >Add Discount to Products</Button> //Apollo useMutation
{emptyState ? (
<Layout>
Select products to continue
</Layout>
) : (
<ChildComponent />
)}
</Page>
);
ChildComponent.js
class ChildComponent extends React {
return(
store.get(bulk-ids).map((product)=>{
<Query query={GET_VARIANTS} variables={{ id: variant }}>
{({ data, extensions, loading, error }) => {
<Layout>
// Query result UI
<Layout>
}}
</Query>
})
)
}
export deafult React.memo(ChildComponent);
React.memo() is useful when your component always renders the same way with no changes. In your case you need to re-render <ChildComponent> every time bulk-id changes. So you should use useMemo() hook.
function parentComponent() {
... rest of code
const bulkIds = store.get('bulk-ids');
const childComponentMemo = useMemo(() => <ChildComponent ids={bulkIds}/>, [bulkIds]);
return <Page>
... rest of render
{bulkIds ?
childComponentMemo
:(
<Layout>
Select products to continue
</Layout>
)}
</Page>
}
useMemo() returns the same value until buldIds has not changed. More details about useMemo() you can find here.
I have the following class that works fine (I know that DOMSubtreeModified is depreacted, I will update this too). It's a very basic WYIWYG I'm trying to refactor to a hook:
export class TextEditorClass extends React.Component {
constructor(props) {
super(props)
this.state ={
// this props contains the html content of the contentEditable
content: this.props.content,
}
}
componentDidMount() {
// here I add a listener to the contentEditable div that calls updateContent
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
document.getElementById("editor").innerHTML = this.props.content;
rangy.init();
}
setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
updateContent() {
this.props.setContent('content', document.getElementById('editor').innerHTML);
this.setState({
content: document.getElementById('editor').innerHTML,
})
}
render() {
return (
<div className='editor-content dashed'>
<input id="myInput" type="file" ref={(ref) => this.upload = ref} style={{ display: 'none' }} />
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>this.setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
</div>
</div>
)
}
}
This is what I wrote so far:
export function TextEditorHook() {
const value = React.useContext(ManagerContext);
React.useEffect(() => {
document.getElementById("editor").innerHTML = value.state.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
rangy.init();
});
function setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
function updateContent() {
value.dispatch({type: 'content', value: document.getElementById('editor').innerHTML});
}
return (
<div className='editor-content dashed'>
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
</div>
</div>
)
}
But it doesn't work:
Warning: A component is changing a controlled input of type text to be
uncontrolled. Input elements should not switch from controlled to
uncontrolled (or vice versa). Decide between using a controlled or
uncontrolled input element for the lifetime of the component.
Second warning:
Warning: Maximum update depth exceeded. This can happen when a
component calls setState inside useEffect, but useEffect either
doesn't have a dependency array, or one of the dependencies changes on
every render.
The reason behind this I suppose is the fact that the typing triggers a loop of rerenders. Why it doesn't happen in the class?
EDIT: I believe the key to fix the issue is in useEffect(); I suppose it behaves differently from componentDidMount, in fact in the hook version I had to swap these two lines to prevent another error from occurring from this:
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
document.getElementById("editor").innerHTML = this.props.content;
to this:
document.getElementById("editor").innerHTML = this.props.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => this.updateContent(), false);
EDIT 2:
Adding [] as a parameter for useEffect() I got rid of the first error:
React.useEffect(() => {
document.getElementById("editor").innerHTML = value.state.content;
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
rangy.init();
}, []);
More info about it here: https://dev.to/trentyang/replace-lifecycle-with-hooks-in-react-3d4n
Your issue does not happen in React class component because componentDidMount is run once and you put the code of componentDidMount in useEffect without any conditions - which means it will run on each render.
Problems with your useEffect:
1) You are adding an event listender but you are not doing a clean-up afterwards.
2)You are mutating the DOM directly which is an anti pattern
3)You are applying any depedencies on your useEffect, which execut the useEffect on each render and will cause a memory leak.
Here is how your code should look like: (will be writing code shortly)
...
const [editorValue, setEditorValue] = React.useState('')
// If you want your hook to run once
React.useEffect( () => {
setEditorValue(value.state.content)
document.getElementById("editor").addEventListener("DOMSubtreeModified", () => updateContent(), false);
// The returning funnction will be executed on unmounting
() => document.getElementById("editor").removeEventListener("DOMSubtreeModified", () => updateContent(), false);
// note the `[]`] as second argument. That means to run only once.
}, [])
And the content of your #editor is then the controllable state editorValue.
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
>
{editorValue}
</div>
Matthew's answer pointed me to the right direction, and after a bit of tweaking I came up with this code that works fine:
import React from 'react'
import { Icon, IconButton, ButtonGroup, ButtonToolbar } from 'rsuite'
import rangy from 'rangy';
import 'rangy/lib/rangy-classapplier';
import dompurify from 'dompurify';
import './TextEditor.css';
import { ManagerContext } from './LessonManager.js';
export function TextEditorHook() {
const value = React.useContext(ManagerContext);
// here I initialize the editor content with the content I receive from the provider
const [editorValue, setEditorValue] = React.useState(value.state.content);
React.useEffect( () => {
rangy.init();
const editor = document.getElementById('editor');
// here I replaced the deprecated DOMSubtreeModified with MutationObserver
// basically when you change the dom of the contentEditable I call dispatch
// and update the content
let mutationObserver = new MutationObserver(function(mutations) {
value.dispatch({type: 'content', value: editor.innerHTML});
});
mutationObserver.observe(editor, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
// the cleanup can be done with .disconnect();
return () => mutationObserver.disconnect();
});
function setApplier(applier) {
rangy.createClassApplier(applier, { elementTagName: "span" }).toggleSelection();
}
// the createMarkup() function is used to generate the markup that I will put inside
// my contentEditable, I installed an additional npm package (dompurify) to clean up
// the markup to prevent XSS attacks
// https://dev.to/jam3/how-to-prevent-xss-attacks-when-using-dangerouslysetinnerhtml-in-react-1464)
function createMarkup() {
const sanitizer = dompurify.sanitize;
return {__html: sanitizer(editorValue)}
};
return (
<div className='editor-content dashed'>
<div className='editor-toolbar'>
<ButtonToolbar>
<ButtonGroup size='xs'>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierBold')}
icon={ <Icon icon="bold"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierItalic')}
icon={ <Icon icon="italic"/> }
/>
<IconButton
className='rsuite-btn'
onClick={()=>setApplier('applierHeader')}
icon={ <Icon icon="header"/> }
/>
</ButtonGroup>
</ButtonToolbar>
</div>
<div
suppressContentEditableWarning={true}
id='editor'
contentEditable
// here I insert the html with the purified html
dangerouslySetInnerHTML={createMarkup()}
>
</div>
</div>
)
}
To give a complete answer I will add also the parent components. This is the LessonContents component:
import React, { useEffect } from 'react';
import { Input } from 'rsuite';
// TextEditorHook is the WYSIWYG component
import { TextEditorHook } from './TextEditor.js';
// this context contain the value of the lesson manager
// we will use the context for the lesson's title, desc, content and location
import { ManagerContext } from './LessonManager.js';
// truncate is a function to shorten text in order to make it fit
// maxLenght is the default max text length
import { truncate, maxLength } from '../Common.js';
export default function Menu() {
const value = React.useContext(ManagerContext);
useEffect(() => {
// if not editing inject the html in the display div on load
if (!value.editing) document.querySelector('.lesson-content').innerHTML = value.state.content;
console.log(value);
});
const editorContent = () => {
return (
<React.Fragment>
<Input
className='rsuite-input dashed'
placeholder='Lesson title'
value={value.state.title}
onChange={(v) => value.dispatch({type: 'title', value: v})}
/>
<Input
className='rsuite-input dashed'
componentClass="textarea"
rows={1}
style={{ width: '100%' }}
placeholder='Lesson description'
value={value.state.desc}
onChange={(v) => value.dispatch({type: 'desc', value: v})}
/>
<TextEditorHook/>
</React.Fragment>
)
}
const viewerContent = () => {
return (
<React.Fragment>
<div className='content'>
<span>{truncate(value.state.title, maxLength, false)}</span>
</div>
<div className='content'>
<span>{value.state.desc}</span>
</div>
<div className='content'>
<span>{truncate(value.state.location, maxLength, false)}</span>
</div>
<div className='content justify-left lesson-content'>
</div>
</React.Fragment>
)
}
return (
<React.Fragment>
{value.editing ? editorContent() : viewerContent()}
</React.Fragment>
)
}
And finally the LessonManager component:
import React from 'react';
import LessonMenu from './LessonMenu.js';
// this context will be used from child components to access the lesson status
import LessonContents from './LessonContents.js';
export const ManagerContext = React.createContext(null);
const initialState = {
title: 'Lesson title',
desc: 'Lesson description',
content: 'Lesson content',
location: 'home / english'
};
function reducer(state, action) {
console.log(action.type, action.value, state)
switch (action.type) {
case 'title':
return {...state, title: action.value};
case 'desc':
return {...state, desc: action.value};
case 'content':
return {...state, content: action.value};
default:
throw new Error();
}
}
export default function LessonManager() {
const [editing, toggleEditor] = React.useState(true);
const [state, dispatch] = React.useReducer(reducer, initialState);
const value = React.useMemo(() => {
return {
state,
dispatch,
editing,
toggleEditor,
}
}, [state, editing]);
return (
<ManagerContext.Provider value={value}>
<div className='box-default expand'>
<div className='handle' style={{display: 'flex', justifyContent: 'center', width: '100%', cursor: 'grab'}}>
<LessonMenu />
</div>
<LessonContents />
</div>
</ManagerContext.Provider>
)
}
I am not completely sure this is the best approach to solve this problem, but it works fine.
I am using React-admin and following the demo that they give out. So far everything is working except for the Tab name/title translation. I have done the translation correctly because other components that have label attribute works fine with the translation.
Translations are getting from en.js file and added to app.js according to the react-admin documentation.
Here is my code :
class TabbedDatagrid extends React.Component {
tabs = [
{ id: 'countries', name: 'root.countries.title' },
{ id: 'languages', name: 'root.languages.title' },
];
state = { countries: [], languages: [] };
static getDerivedStateFromProps(props, state) {
if (props.ids !== state[props.filterValues.status]) {
return { ...state, [props.filterValues.status]: props.ids };
}
return null;
}
handleChange = (event, value) => {
const { filterValues, setFilters } = this.props;
setFilters({ ...filterValues, status: value });
};
render() {
const { classes, filterValues, ...props } = this.props;
return (
<Fragment>
<Tabs
fullWidth
centered
value={filterValues.status}
indicatorColor="primary"
onChange={this.handleChange}
>
{this.tabs.map(choice => (
<Tab
key={choice.id}
label={choice.name}
value={choice.id}
/>
))}
</Tabs>
<Divider />
<Responsive
small={<SimpleList primaryText={record => record.title} />}
medium={
<div>
{filterValues.status === 'countries' && (
<Datagrid hover={false}
{...props}
ids={this.state['countries']}
>
<TextField source="id" />
<TextField source="name" label="root.countries.fields.name"/>
</Datagrid>
)}
{filterValues.status === 'languages' && (
<Datagrid hover={false}
{...props}
ids={this.state['languages']}
>
<TextField source="id" />
<TextField source="name" label="root.languages.fields.name"/>
</Datagrid>
)}
</div>
}
/>
</Fragment>
);
}
}
The translations seems to work everywhere else but the Tab label, What I get instead of the Title is uppercase string of this root.countries.title.
Is there a workaround or how to fix this issue?
You probably used <Tab/> 'directly' from material-ui.
You need to use (create) 'enhanced version' (using translate prop) of this component.
Take inspiration from menu or other translatable components.
You need to pass your translations to your App.js as follows :
import React from 'react';
import { Admin, Resource } from 'react-admin';
import frenchMessages from 'ra-language-french';
import englishMessages from 'ra-language-english';
const messages = {
fr: { component:{label:'test'},...frenchMessages },
en: { component:{label:'test'},...englishMessages },,
}
const i18nProvider = locale => messages[locale];
const App = () => (
<Admin locale="en" i18nProvider={i18nProvider}>
...
</Admin>
);
export default App;
than when you want to use translations inside a component, you need to connect it to the react-admin's translate function as follows :
import { TextInput, translate } from 'react-admin';
const translatedComponent = ({translate, ...props}) => {
return <TextInput label={translate('component.label')} />
}
export default translate(translatedComponent);
it is important to connect the component with translate and to get the translate function from props to get the translation work.