Move a React element within the DOM hierarchy while keeping state - javascript

I have a small code experiment - a React based text editor, with a component <TextEditor /> which keeps its text and other information in state. When the user presses the keyboard shortcut for 'split editor', I replace it with an element like:
<Split
left={this.editors[0]}
right={this.editors[1]} />
Where editors is an array like this.editors = [<TextEditor />, <TextEditor />] setup in the constructor of the parent <App /> element.
However, when switching from rendering this.editors[0] to a split with this.editors[0] as the left element, the editor looses state (clearing its text), even though the actual JSX I'm rendering comes from the same element in the array.
How can I move a React element deeper into the hierarchy without loosing state?
The actual code for rendering the app is like this:
import React, { Component, Fragment } from 'react'
import Split from './Components/Split'
import TextEditor from './Components/TextEditor'
class App extends Component {
constructor (props) {
super(props)
this.state = {
editorPanes: 0
}
this.openNewPanel = this.openNewPanel.bind(this)
this.editors = [<TextEditor openNewPanel={this.openNewPanel} />]
}
getJSXForPane (p) {
if (typeof p === 'number') {
return this.editors[p]
} else {
return (
<Split
vertical={p.vertical}
left={this.getJSXForPane(p.left)}
right={this.getJSXForPane(p.right)}
/>
)
}
}
openNewPanel () {
const newEditorId = this.editors.length
this.editors.push(<TextEditor openNewPanel={this.openNewPanel} />)
this.setState({
editorPanes: {
vertical: true,
left: this.state.editorPanes,
right: newEditorId
}
})
}
render () {
return (
<Fragment>
{this.getJSXForPane(this.state.editorPanes)}
</Fragment>
)
}
}

Related

React - how to append react component to useRef component during useEffect

I would like to know if there is a way to append another react component to the useRef element?
Scenario: when the Parent's useEffect detects the Child's heading to be bigger than X size: add another react component.
I would like the implementation to be on the Parent, because in my whole app, there was only one specific case where I need to do this. Therefore I did not want to modify the core Child component props.
import React, { ReactNode, useEffect, useRef } from 'react';
import { css } from 'emotion';
const someStyle = css`
background-color: red;
`;
type ChildProp = {
children: ReactNode;
};
const Child = React.forwardRef<HTMLHeadingElement, ChildProp>(
({ children }, ref) => {
return <h1>{children}</h1>;
},
);
const Parent = React.FunctionComponent = ()=> {
const childRef = useRef<HTMLHeadingElement>(null);
useEffect(() => {
if (childRef.current && childRef.current.clientHeight > 30) {
// append component to childRef.current
// e.g. childRef.current.append(<div className={someStyle}>hello</div>);
}
}, []);
return <Child ref={childRef}>hello world</Child>;
};
export default Parent;
You should not manually manipulate the dom. React makes the assumption that it's the only one changing the page, so if that assumption is false it may make changes which overwrite yours, or skip changes that it doesn't realize it needs to do.
Instead, you should set state, causing the component to rerender. Then render something that matches what you want the dom to look like.
const Parent = React.FunctionComponent = ()=> {
const childRef = useRef<HTMLHeadingElement>(null);
const [large, setLarge] = useState(false);
useEffect(() => {
if (childRef.current && childRef.current.clientHeight > 30) {
setLarge(true);
}
}, []);
return (
<React.Fragment>
<Child ref={childRef}>hello world</Child>
{large && (<div className={someStyle}>hello</div>)}
</React.Fragment>
)
};
P.S: If you see the screen flicker with this code, consider changing it to useLayoutEffect instead of useEffect

scroll into view after routing in react

How to scroll into view after routing in react
We are using react-router. What I want to achieve is do a scroll into view on one of component after react route to that page.
Here's a quick example for you using react-router-dom and refs. You didn't provide any code for us to look at so consider this a template. :)
Also here's a sandbox for your reference: https://codesandbox.io/s/adoring-surf-h55ci
So let's say your Routes are set-up like this:
Routes
function App() {
return (
<BrowserRouter>
<div>
<Route path="/" exact component={Home} />
<Route path="/example" component={Example} />
</div>
</BrowserRouter>
);
}
So user starts out at Home "/"
import React from "react";
import { Link } from "react-router-dom";
const Home = () => {
return <Link to="/example">Go To Example</Link>;
};
export default Home;
They click on the link and it takes them to the /example route, rendering Example
import React from "react";
import Section from "./Section";
class Example extends React.Component {
componentDidMount() {
if (this.mySection.current) {
this.mySection.current.scrollIntoView({
behavior: "smooth",
nearest: "block"
});
}
}
mySection = React.createRef();
render() {
return (
<div>
<div style={{ background: "orange", height: "750px" }}>
This is an example, below is my component.
</div>
<div ref={this.mySection}>
<Section />
</div>
</div>
);
}
}
export default Example;
Example has two parts, a div with plain text and the Section component. As you noticed, we wrap the Section component in a div and gave it a ref prop. The ref lets us communicate with that wrapper-div. In componentDidMount(), we just scroll to that div.
One way to scroll to a component would be to put a ref on the component you want to scroll to:
https://reactjs.org/docs/refs-and-the-dom.html
Once you have put the ref onto the component, you could then scroll to your ref in the componentDidMount() of the parent component, something like:
window.scrollTo(0, this.myRef.current.offsetTop)
You may need to be slightly more defensive here, and do something like this:
this.myRef && window.scrollTo(0, this.myRef.current.this.myRef)
This way, when the route is visited, the component will be scrolled to its offsetTop
All of these solution seem to be wrong
what expected is on click -> route -> scroll into a view
With above examples what will happen is on any render you scroll a particular element into a view...
The behaviour for general componentDidMount should stay as is ...
You can either add a hash to a routing to distinguish if it's a render from let's say page a - > then on page itself check if there is param in query.
Then the behaviour will be clean for componentDidMount.
For the above answers which i don't find correct you can use
useSmoothScroll(
target: string,
options: {
freezeStickyComponents?: boolean
hashLocation?: string
offset?: number | boolean
preventDefault?: boolean
} = {},
callback: (hash?: string) => void = DEFAULT_CALLBACK
): UseSmoothScrollCallback {
const defaultOptions = {
freezeStickyComponents: true,
hashLocation: null,
offset: false,
preventDefault: true
}
options = { ...defaultOptions, ...options }
const hasHash = options.hashLocation || target
const hash = hasHash && `${hasHash}`
const { freeze } = useStickyState()
return useCallback(
(event: SyntheticEvent<HTMLElement>): void => {
if (options.preventDefault) {
event.preventDefault()
}
if (!target) {
scrollToTop(callback)
return
}
const reference = document.getElementById(target)
if (typeof options.offset === 'number') {
window.scroll({
top:
reference?.getBoundingClientRect().top +
window.scrollY -
options.offset,
behavior: 'smooth'
})
} else {
reference?.scrollIntoView({
behavior: 'smooth'
})
}
window.history.pushState(null, null, hash)
callback(hash)
if (options.freezeStickyComponents) {
freeze(800)
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[freeze, hash, options, target]
)
}

Declaratively update markup inside component

I have a component in my app that renders some data, most commonly a page title.
Markup:
<Toolbar>
<ToolbarRow>
<div id="title-bar">
{children}
</div>
</ToolbarRow>
</Toolbar>
How would I declaratively be able to change the data inside?
I've tried react-side-effects which allowed me to indeed change the title to be rendered but then I wanted to be able to add components as well.
Components aren't to be stored inside state so there's that…
Then I looked at Portals, which seem to exactly what I want but I get Target container is not a DOM element.
Markup for the portal component:
import React from "react";
import {createPortal} from 'react-dom'
const PageTitle = ({title, children}) => {
return createPortal(
<p>foo</p>,
document.getElementById('title-bar')
)
};
export default PageTitle;
I'm calling the portal component like so:
<PageTitle title="Document Overview"/>
As you can see from the above snippet, the other component adds a <div id="title-bar" />, so I guess it has to do with timing.
Anyone have a good idea?
I would just put components into the state here:
const bars = [];
export class TitleBar extends Component {
state = { children: [] };
componentDidMount() { bars.push(this); }
componentWillUnmount() { bars.splice(bars.indexOf(this), 1); }
render() { return this.state.children };
}
const RealPageTitle = ({ title }) => <div> { title } </div>;
export class PageTitle extends Component {
constructor(props) {
super(props);
this.real = RealPageTitle(props);
}
componentDidMount() {
for(const bar of bars)
bar.setState(({ children }) => ({ children: children.concat(this.real) }));
}
componentWillUnmount() {
for(const bar of bars)
bar.setState(({ children }) => ({ children: children.filter(child => child !== this.real) }));
}
render() { }
}
That way you can just add <PageTitle title={"Test"} /> somewhere on the page and it gets added to the title bar.
I know this does not follow "best practices", but it certainly works

ReactJS: How to get state property of another component?

There is a main component, which uses a menu component. The menu component is using a state property to save the information about selected menu item. But now I need to get the selected module in the main component. How do I do that?
class Main extends Component {
doSomething(module) {
console.log(module) // should get 'targetValue'
// I need to get the info, which module is selected.
// This info is stored as a state value in the `MainMenu` Component
// How do I get this information? I can't use the parameter `selectModule` as it is done here.
}
render() {
return (
<div>
<MainMenu />
<Button
onClick={ this.doSomething.bind(this, selectedModule) }
/>
</div>
)
}
}
In this component a menu is generated for each module (of modules array). By clicking on one item, this module is stored into module state variable.
class MainMenu extends Component {
constructor(props) {
super(props)
this.state = {
module: 'initialValue'
}
}
selectModule(module) {
this.setState({ module })
}
render() {
return (
<Menu>
<Menu.Item onClick={ this.selectModule.bind(this, 'targetValue') } >
{ title }
</Menu.Item>
</Menu>
)
}
}
Instead of doing some magic and examining internal state if children components lift the state to parent. Child becomes stateless.
class Main extends Component {
state = {
module: 'initialValue'
}
setActiveModule = (module) => {
this.setState({ module })
}
render() {
return (
<MainMenu onChange={this.setActiveModule} />
)
}
}
class MainMenu extends Component {
onClick = (module) => () => {
this.props.onChange(module)
}
render() {
return (
<Menu>
<Menu.Item onClick={this.onClick(title)} >
{title}
</Menu.Item>
</Menu>
)
}
}
Instead on maintaining the state in MainMenu component, maintain in parent component Main, and pass the module value in props, also pass a function to MainMenu to update the state of parent component Main from child MainMenu.
Write it like this:
class Main extends Component {
constructor(props) {
super(props)
this.state = {
module: 'initialValue'
}
this.update = this.update.bind(this);
}
update(value){
this.setState({
module: value
});
}
doSomething(){
console.log(this.state.module);
}
render() {
return (
<div>
<MainMenu module={this.state.module} update={this.update}/>
<Button
onClick={ this.doSomething.bind(this) }
/>
</div>
)
}
}
class MainMenu extends Component {
selectModule(module) {
this.props.update(module);
}
render() {
console.log(this.props.module);
return (
<Menu>
<Menu.Item onClick={this.selectModule.bind(this, 'targetValue') } >
{ title }
</Menu.Item>
</Menu>
)
}
}
Sharing state with react is sometimes a bit hard.
The react philosophy tends to say that we have to manage state from top to bottom. The idea is to modify the state in your parent, and pass the informations as props. For example, let's imagine the following scenario :
class Main extends React.Component {
contructor(props) {
super(props);
this.state = { currentMenuSelected: 'Home' };
}
onPageChange(newPage) {
this.setState({ currentMenuSelected: newPage });
}
render() {
return(
<div>
<AnotherComponent currentMenu={this.state.currentMenuSelected} />
<MenuWrapper onMenuPress={this.onPageChange} />
</div>
)
}
}
In my example, we tell the MenuWrapper to use the Main.onPageChange when changing page. This way, we're now able to pass that current selected menu to AnotherComponent using props.
This is the first way to manage state sharing using react, and the default one provided by the library
If you want to manage more complex stuff, sharing more state, you should take a look at the flux architecture https://facebook.github.io/flux/docs/overview.html
and the most common implementation of flux : http://redux.js.org/
Store the menu state in the main component, and pass the state updater down to the menu.
This is quite helpful in getting into top-down state
https://facebook.github.io/react/docs/thinking-in-react.html

Using React's shouldComponentUpdate with Immutable.js cursors

I'm having trouble figuring out how to short circuit rendering a branch
of a tree of React components using Immutable.js cursors.
Take the following example:
import React from 'react';
import Immutable from 'immutable';
import Cursor from 'immutable/contrib/cursor';
let data = Immutable.fromJS({
things: [
{title: '', key: 1},
{title: '', key: 2}
]
});
class Thing extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.thing.deref() !== nextProps.thing.deref();
}
handleChangeTitle(e) {
this.props.thing.set('title', e.target.value);
}
render() {
return <div>
<input value={this.props.thing.get('title')}
onChange={this.handleChangeTitle.bind(this)} />
</div>;
}
}
class Container extends React.Component {
render() {
const cursor = Cursor.from(this.props.data, 'things', newThings => {
data.set('things', newThings);
renderContainer();
});
const things = cursor.map(thing => (
<Thing thing={thing} key={thing.get('key')} />
));
return <div>
{things}
</div>;
}
}
const renderContainer = () => {
React.render(<Container data={data} />, document.getElementById('someDiv'));
};
Say I change the first Thing's title. Only the first Thing will render with
the new title and the second Thing will not re-render due to
shouldComponentUpdate. However, if I change the second Thing's title, the
first Thing's title will go back to '' since the second Thing's cursor
is still pointing at an older version of the root data.
We update the cursors on each render of Container but the ones that don't
render due to shouldComponentUpdate also don't get the new cursor with the updated
root data. The only way I can see keeping the cursors up to date is to remove
shouldComponentUpdate in the Thing component in this example.
Is there a way to change this example to use shouldComponentUpdate using fast referential
equality checks but also keep the cursors updated?
Or, if that's not possible, could you provide an overview of how you would generally work with cursors + React components and rendering only components with updated data?
I updated your code, see comments inline:
class Thing extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.thing.deref() !== nextProps.thing.deref();
}
handleChangeTitle(e) {
// trigger method on Container to handle update
this.props.onTitleChange(this.props.thing.get('key'), e.target.value);
}
render() {
return <div>
<input value={this.props.thing.get('title')}
onChange={this.handleChangeTitle.bind(this)} />
</div>;
}
}
class Container extends React.Component {
constructor() {
super();
this.initCursor();
}
initCursor() {
// store cursor as instance variable to get access from methods
this.cursor = Cursor.from(data, 'things', newThings => {
data = data.set('things', newThings);
// trigger re-render
this.forceUpdate();
});
}
render() {
const things = this.cursor.map(thing => (
<Thing thing={thing} key={thing.get('key')} onTitleChange={this.onTitleChange.bind(this)} />
));
return <div>
{things}
</div>;
}
onTitleChange(key, title){
// update cursor to store changed things
this.cursor = this.cursor.update(x => {
// update single thing
var thing = x.get(key - 1).set('title', title);
// return updated things
return x.set(key - 1,thing);
});
}
}
const renderContainer = () => {
React.render(<Container data={data} />, document.getElementById('someDiv'));
};

Categories