Using react components to render data controlled by third party library - javascript

I'm building a React component that needs to consume data from a tree library built in vanilla JS. This library holds and manages data for all "tree nodes" and they're state - expanded/collapsed, selected, hidden, etc.
I'm unsure how to approach building react components because they ideally control their own state or use a store designed for use in react.
Here's a super simple example of data that might be loaded into the tree library.
[{
id: 1,
text: 'Node 1'
}, {
id: 2
text: 'Node 2',
state: {
selected: true
}
}]
It gets loaded into the tree lib via the constructor new Tree(nodes); and the tree lib provides a ton of methods to work with it: tree.deselect(2) and tree.selected() // -> []
I've toyed around with some basic components to render this example:
I start with <TreeNodes nodes={tree.nodes()} />
const TreeNodes = ({ nodes }: TreeNodesProps) => {
return (<>{ nodes.map(node => <TreeNode key={node.id} node={node} />) }</>);
}
const TreeNode = ({ node }: TreeNodeProps) => {
const onClick = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
node.toggleSelect();
}
return <div className={clsx({ selected: node.selected()})} onClick={onClick}>{node.text}</div>
}
The tree library fires events like node.selected to let me know when something has changed in the data.
My question is, what's the best/proper way to then sync my data to react components?
I was debating listening for all tree events and updating a state object in the root component but that feels wrong:
const [nodes, setNodes] = useState(tree.nodes());
this.tree.on('node.selected', () => {
setNodes(tree.nodes())
});

I honestly don't feel adding a listener is wrong as long as it works fine. Also I think you should wrap it up in a useEffect this way:
function MyApp() {
const [nodes, setNodes] = useState(tree.nodes());
useEffect(() => {
tree.on("node.selected", () => {
setNodes(tree.nodes());
});
return () => {/* remove listener here */}
}, []);
}
You either have this solution or you will have to make all changes to the data by yourself.

Related

Reducing renders in child components of a context

I'm trying to delve deeper than the basics into React and am rebuilding a Tree library I had written in plain JS years back. I need to expose an API to users so they can programmatically add/remove nodes, select nodes, etc.
From what I've learned, a ref and context is a good approach. I've built a basic demo following the examples (without ref, for now) but I'm seeing every single tree node re-render when a selection is made, even though no props have changed for all but one.
I've tried a few things like memoizing my tree node component, etc but I feel like I'm failing to understand what's causing the re-render.
I'm using the react dev tools to highlight renders.
Here's a codesandbox demo.
My basic tree node component. I essentially map this for every node I need to show. On click, this calls select() from my context API. The rerender doesn't happen if that select() call is disabled.
const TreeNodeComponent = ({ id, text, children }) => {
console.log(`rendering ${id}`);
const { select } = useTreeContext();
const onClick = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
select([id]);
},
[select, id]
);
return (
<div className="tree-node" onClick={onClick}>
{text}
{children ? <TreeNodesComponent nodes={children} /> : ""}
</div>
);
};
The important part of my context is the provider:
const TreeContextProvider = ({ children, nodes = [], selectedNodes }) => {
const [allNodes] = useState(nodes);
const [allSelectedNodes, setSelectedNodes] = useState(selectedNodes || []);
const api = useMemo(
() => ({
selected: () => allSelectedNodes,
select: (nodes) => {
setSelectedNodes(Array.from(new Set(allSelectedNodes.concat(nodes))));
}
}),
[allSelectedNodes]
);
const value = useMemo(
() => ({
nodes: allNodes,
selectedNodes: allSelectedNodes,
...api
}),
[allNodes, allSelectedNodes, api]
);
return <TreeContext.Provider value={value}>{children}</TreeContext.Provider>;
};
Again, when the call to setSelectedNodes is disabled, the rerenders don't happen. So the entire state update is triggering the render, yet individual props to all but one tree component do not change.
Is there something I can to improve this? Imagine I have 1000 nodes, I can't rerender all of them just to mark one as selected etc.

Reselect: createSelector not working correctly

I have a problem with my memoized selectors.
Reading the docs on https://redux.js.org/usage/deriving-data-selectors
I taken this snippets:
const state = {
a: {
first: 5
},
b: 10
}
const selectA = state => state.a
const selectB = state => state.b
const selectA1 = createSelector([selectA], a => a.first)
const selectResult = createSelector([selectA1, selectB], (a1, b) => {
console.log('Output selector running')
return a1 + b
})
const result = selectResult(state)
// Log: "Output selector running"
console.log(result)
// 15
const secondResult = selectResult(state)
// No log output
console.log(secondResult)
// 15
My problem is that the secondResult function, log the result.
All this is a little premise.
My very problem:
I'am using react with #reduxjs/toolkit
I have a list of todo
I created a "todoAdapter" to manage a list as entities
I want to use memoized selected to update a single "todo" without re-render the entire list optimizing my app.
but...
When I dispatch an update with "adapter.updateOne", the standard selector "SelectAll" every time (i think) changes ref.
Example
I have this selectors from "slice"
export const {
selectAll,
selectIds: selectTodosIDs,
selectTotal: selectTodosCount,
selectById: selectTodoById
} = todosSelector;
If I create this selector
export const selectIdsCustom = createSelector(
selectTodosIDs,
(ids) => {
console.log('execute output function');
return ....
}
)
It' all ok (state.todos.ids not change obviously).
If I create this selector:
export const selectTodosCustom = createSelector(
selectAll,
(todos) => {
console.log('execute output function');
return ....
}
)
selectTodosCustom run "always".
Whyyy???
With updateOne I am modifing "only" an entity inside "state.todos.entities"
Where am I wrong ??
What I did not understand?
My app is just un case study. The complete app is on:
https://codesandbox.io/s/practical-hermann-60i7i
I only created the same app in typescript and, when I as my app had this problem:
I download the original app form link above (official redux example)
npm install
npm start
But I have the some problem also in the official example!!!!!
Problem is my env? some library version?
When I dispatch an update with "adapter.updateOne", the standard
selector "SelectAll" every time (i think) changes ref.
Yes, you are right. It is correct.
selectAll from #reduxjs/toolkit depends on ids and entities from the entities state.
{
ids: ['1', '2'],
entities: {
1: {
id: '1',
title: 'First',
},
2: {
id: '2',
title: 'Second',
},
},
}
Every time you dispatch an update with adapter.updateOne, the reference to the entities object changes. This is normal, this is how immerjs (used under the hood of reduxtoolkit) provides correct immutability:
dispatch(updateOne({ id: '1', changes: { title: 'First (altered)' } }));
const state = {
ids: ['1', '2'],
entities: { // <--- new object
1: { // <--- new object
id: '1',
title: 'First (altered)',
},
2: { // <--- old object
id: '2',
title: 'Second',
},
},
};
If the entities object remained old, the selector selectAll would return a memoized value with an incorrect title for the first element.
To optimize the re-render of the list (which in actually useful only for large lists), you should use selector selectIds in the parent component and selector selectById in the child components.
const Child = ({ id }) => {
const dispatch = useDispatch();
const book = useSelector((state) => selectById(state, id));
const handleChange = useCallback(() => {
// the parent component will not be re-render
dispatch(
bookUpdate({
id,
changes: {
title: `${book.title} (altered)`,
},
})
);
}, [dispatch, id, book]);
return (
<div>
<h2>{book.title}</h2>
<button onClick={handleChange}>change title</button>
</div>
);
};
function Parent() {
const ids = useSelector(selectIds);
return (
<div>
{ids.map((id) => (
<Child key={id} id={id}></Child>
))}
</div>
);
}
UPDATE
In this case, item not change ref and in "entities" I am changing a
single prop inside it.
It is not right???
No, you can't do that when using redux. Redux is based on the idea of immutability. Any state change creates a new state object.
Updating an entity is just a deep change of the state object. And all objects on the path to the updated element must be new.
If you do not follow this rule, then base tools will not work correctly. All of them use strict comparison by default.
But you can still avoid re-render the component even when the selector returns the equal arrays with different references. Just pass your own comparison function:
...
const todoIds = useSelector(
selectFilteredTodoIds,
// the component will be re-render only if this function returns false
(next, prev) => next.every((id, index) => id === prev[index])
);
...

React hook use class object as useState

A bit new to React here.
While developing a personal project based on React, I often came up against scenarios where I needed child components to update state passed down to it by a parent, and have the updated state available in both child and parent components.
I know React preaches a top-down flow of immutable data only, but is there an elegant way of solving this issue?
The way I came up with is as shown below. It gets the job done, but it looks and feels ugly, bloated, and it makes me think I'm missing something obvious here that a well-established framework like React would've definitely accounted for in a more intuitive way.
As a simple example, assume that I have a couple of nested components:
Root
Author
Post
Comment
And that I need each child component to be able to modify the state such that it is also accessible to its parent component as well. A use case might be that you could interact with the Comment component to edit a comment, and then interact with a SAVE button defined in the Root component to save the entire state to a database or something like that.
The way I presently handle such scenarios is this:
const Root = ({}) => {
const [data, setData] = React.useState({
author: {
name: 'AUTHOR',
post: {
content: 'POST',
comment: {
message: 'COMMENT'
}
}
}
});
const onEditAuthor = value => {
setData({ author: value });
}
const onSave = () => {
axios.post('URL', data);
}
return <>
<Author author={data.author} onEditAuthor={onEditAuthor} />
<button onClick={() => onSave()}>SAVE</button>
</>
}
const Author = ({ author, onEditAuthor }) => {
const onEditPost = value => {
onEditAuthor({ name: author.name, post: value });
}
return <Post post={author.post} onEditPost={onEditPost} />
}
const Post = ({ post, onEditPost }) => {
const onEditComment = value => {
onEditPost({ content: post.content, comment: value });
}
return <Comment comment={post.comment} onEditComment={onEditComment} />
}
const Comment = ({ comment, onEditComment }) => {
return <input defaultValue={comment.message} onChange={ev => onEditComment({ message: ev.target.value })} />
}
When you change the Comment, it calls the Post.onEditComment() handler, which in turn calls the Author.onEditPost() handler, which finally calls the Root.onEditAuthor() handler. This finally updates the state, causing a re-render and propagates the updated state all the way back down.
It gets the job done. But it is ugly, bloated, and looks very wrong in the sense that the Post component has an unrelated onEditComment() method, the Author component has an unrelated onEditPost() method, and the Root component has an unrelated onEditAuthor() method.
Is there a better way to solve this?
Additionally, when the state finally changes, all components that rely on this state are re-rendered whether they directly use the comment property or not, as the entire object reference has changed.
I came across https://hookstate.js.org/ library which looks awesome. However, I found that this doesn't work when the state is an instance of a class with methods. The proxied object has methods but without this reference bound properly. I would love to hear someone's solution to this as well.
Thank you!
There's nothing wrong with the general idea of passing down both a value and a setter:
const Parent = () => {
const [stuff, setStuff] = useState('default stuff')
return (
<Child stuff={stuff} setStuff={setStuff} />
)
}
const Child = ({ stuff, setStuff }) => (
<input value={stuff} onChange={(e) => setStuff(e.target.value)} />
)
But more in general, I think your main problem is attempting to use the shape of the POST request as your state structure. useState is intended to be used for individual values, not a large structured object. Thus, at the root level, you would have something more like:
const [author, setAuthor] = useState('AUTHOR')
const [post, setPost] = useState('POST')
const [comment, setComment] = useState('CONTENT')
const onSave = () => {
axios.post('URL', {
author: {
name: author,
post: {
content: post,
comment: {
message: comment,
},
},
},
})
}
And then pass them down individually.
Finally, if you have a lot of layers in between and don't want to have to pass a bunch of things through all of them (also known as "prop drilling"), you can pull all of those out into a context such as:
const PostContext = createContext()
const Root = () => {
const [author, setAuthor] = useState('AUTHOR')
const [post, setPost] = useState('POST')
const [comment, setComment] = useState('CONTENT')
const onSave = useCallback(() => {
axios.post('URL', {
author: {
name: author,
post: {
content: post,
comment: {
message: comment,
},
},
},
})
}, [author, comment, post])
return (
<PostContext.Provider
value={{
author,
setAuthor,
post,
setPost,
comment,
setComment,
}}
>
<Post />
</PostContext.Provider>
)
}
See https://reactjs.org/docs/hooks-reference.html#usecontext for more info
Your example illustrates the case of "prop drilling".
Prop drilling (also called "threading") refers to the process you have to go through to get data to parts of the React Component tree.
Prop drilling can be a good thing, and it can be a bad thing. Following some good practices as mentioned above, you can use it as a feature to make your application more maintainable.
The issue with "prop drilling", is that it is not really that scalable when you have an app with many tiers of nested components that needs alot of shared state.
An alternative to this, is some sort of "global state management" - and there are tons of libraries out there that handles this for you. Picking the right one, is a task for you 👍
I'd recommend reading these two articles to get a little more familiar with the concepts:
Prop Drilling
Application State Management with React
I would use redux instead of doing so much props work, once you create your states with redux they will be global and accesible from other components. https://react-redux.js.org/

event bus in React?

I'm mainly using Vue, and just recently picked up React. Loving it so far, and its quite similar in a lot of ways to Vue, which makes learning it way easier.
Now, let's consider two siblings component. I want to trigger something in component number one, when something happens in component number two. In Vue you can just bind window.bus = new Vue, and then emit in one of the components bus.$emit('event') and bind in the mounted() of the second component bus.$on('event', this.doSth).
How can you achieve that in React?
Event Bus is only a Global Function Register, can you use it
class _EventBus {
constructor() {
this.bus = {};
}
$off(id) {
delete this.bus[id];
}
$on(id, callback) {
this.bus[id] = callback;
}
$emit(id, ...params) {
if(this.bus[id])
this.bus[id](...params);
}
}
export const EventBus = new _EventBus();
The export const prevent multiple instances, making the class static
In the case of two sibling components, you would hold the state in the parent component and pass that state as a prop to both siblings:
class ParentComponent extends Component {
state = {
specialProp: "bar"
}
changeProp = () => {
// this.setState..
}
render() {
return (
<div>
<FirstSibling specialProp={this.state.specialProp} />
<SecondSibling changeProp={this.changeProp} specialProp={this.state.specialProp} />
</div>
);
}
}
For those that are still searching, look at this article: https://www.pluralsight.com/guides/how-to-communicate-between-independent-components-in-reactjs
The solution uses:
document.addEventListener for $on;
document.dispatchEvent for $emit;
document.removeEventListener for $off;
In my use case I had a component that contained a Refresh button that would trigger a data refresh in other components.
I did everything as it was presented in the article. In case you're using React with Functional Components, you might want to use the following useEffect:
useEffect(() => {
// The component might unmount when the request is in progress.
// Because of this an error will appear when we'll try to 'setData'.
let isMounted = true
const getData = () => {
reqData(db, path)
.then(result => {
if (isMounted) {
setData(result) // This is a useState
}
})
}
eventBus.on('refreshData', (data) => {
getData()
})
getData()
return () => {
isMounted = false
eventBus.remove('refreshData')
}
// Do not change the end of this useEffect. Keep it as it is. It simulates componentDidMount.
// eslint-disable-next-line
}, [])
A parent component can manage the state and methods consumed by child components when passed down through props.
The following example increments a count. SibOne displays the count and a button in SibTwo increments the count.
class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
incrementCount = () => {
this.setState({
count: this.state.count + 1
});
}
render() {
return (
<div className="App">
<SibOne count={this.state.count}/>
<SibTwo incrementCount={this.incrementCount}/>
</div>
);
}
}
const SibOne = props => <div>Count: {props.count}</div>;
const SibTwo = props => (
<button onClick={props.incrementCount}>
Increment Count
</button>
);
Demo: https://codesandbox.io/s/zqp9wj2n63
More on Components and Props: https://reactjs.org/docs/components-and-props.html
Write-Once, use everywhere Event Bus (PubSub) system
I have used Vue for a long time and I enjoyed using the Event Bus too, was a pain that it wasn't readily available on react. Although, after a lot of research, I came up with an approach that should be easy to implement.
You can leverage Event Listeners and the CustomEvent API to implement the Event Bus architecture.
// event-bus.js file
const EventBus = {
$on (eventType, callback) {
document.addEventListener(eventType, (ev) => callback(ev.detail))
},
$dispatch (eventType, data) {
const event = new CustomEvent(eventType, { detail: data })
document.dispatchEvent(event)
},
$remove (eventType, callback) {
document.removeEventListener(eventType, callback)
}
}
export default EventBus
You can now go ahead to import the EventBus object to any file of choice, here is what the usage looks like for me.
For the publisher:
// publisher.js file
EventBus.$dispatch(message.name, message.value)
And on the subscriber's end:
// subscriber.js file
EventBus.$on('user.connect', (data) => console.log(data))
rxjs is one more option that you can explore. check if that can help you as they are providing observable and event based programs. It is also doing similar thing like what Eventbus is doing in vuejs.

React.js "global" component that can be created multiple times

I can't get my head wrapped around this.
The problem: let's say there's an app and there can be some sort of notifications/dialogs/etc that i want to create from my code.
I can have "global" component and manage it, but it would limit me to only one notification at a time, this will not fit.
render() {
<App>
// Some components...
<Notification />
</App>
}
Or i can manage multiple notifications by the component Notification itself. But state management will not be clear.
The other problem if i have some sort of user confirmation from that component (if it's a confirmation dialog instead of simple notification) this will not be very convinient to handle with this solution.
The other solution is to render a component manually. Something like:
notify(props) {
const wrapper = document.body.appendChild(document.createElement('div'))
const component = ReactDOM.render(React.createElement(Notification, props), wrapper)
//...
// return Promise or component itself
}
So i would call as:
notify({message: '...'})
.then(...)
or:
notify({message: '...', onConfirm: ...})
This solution seems hacky, i would like to let React handle rendering, and i have an additional needless div. Also, if React API changes, my code breaks.
What is the best practice for this scenario? Maybe i'm missing something completely different?
You could use React Context for this.
You create a React context at a high level in your application and then associate a values to it. This should allow components to create / interact with notifications.
export const NotificationContext = React.createContext({
notifications: [],
createNotification: () => {}
});
class App extends Component {
constructor() {
super();
this.state = {
notifications: []
};
this.createNotification = this.createNotification.bind(this);
}
createNotification(body) {
this.setState(prevState => ({
notifications: [body, ...prevState.notifications]
}));
}
render() {
const { notifications } = this.state;
const contextValue = {
notifications,
createNotification: this.createNotification
};
return (
<NotificationContext.Provider value={contextValue}>
<NotificationButton />
{notifications.map(notification => (
<Notification body={notification} />
))}
</NotificationContext.Provider>
);
}
}
The notifications are stored in an array to allow multiple at a time. Currently, this implementation will never delete them but this functionality can be added.
To create a notification, you will use the corresponding context consumer from within the App. I have added a simple implementation here for demonstration purposes.
import { NotificationContext } from "./App.jsx";
const NotificationButton = () => (
<NotificationContext.Consumer>
{({ notifications, createNotification }) => (
<button onClick={() => createNotification(notifications.length)}>
Add Notification
</button>
)}
</NotificationContext.Consumer>
);
You can view the working example here.

Categories