-
Notifications
You must be signed in to change notification settings - Fork 558
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Signals and Slots Hooks #135
Conversation
Look like this could be implemented in a user-space with ease. Just one |
Would you mind posting a code example of that, preferably something similar to what I have above? You are right that it could be implemented in user-space, but this seems easier and more natural to use. Context requires creating a provider and exporting the context so that other components can use it. It involves a lot of extra code, and it can be difficult to determine what the code is doing just by looking at it. Almost anyone could understand the example above. |
@Symbitic I think a recent RFC (and the user-land solutions mentioned in it) seem to be very suitable to solve this problem: #130 E.g. checkout https://github.com/pie6k/hooksy It's not a one-to-one mapping to your proposal, but arguably a more natural way to achieve the same thing |
I'm not sure I entirely follow what const MyButton = Button;
const MyBanner = Banner;
connect(MyButton, 'clicked', MyBanner, 'message'); would do. Surely the first two lines merely alias the |
@FireyFly I don't know enough about the internals of React to say for sure. That was just meant to be an example of connecting to specific instances of components. If you have any alternate syntaxes feel free to share them. @everdimension I checked hooky and all the others listed in that RFC. Unfortunately, I don't think any of them are substitutes for this. All of them are are for sharing state, which is arguably something that falls outside the scope of React. Signals and Slots are for allowing components to communicate with each other without being binding them. The only state for each component should be its own internal state. Having state be managed outside your scope (or managing another components state) inherently leads to tighter coupling. Signals and Slots is meant to solve the problem of inter-component communication, not cross-component state management. If you feel differently about any of this feel free to explain why. |
It doesn't really do that, because the reassignment still points to the same reference. |
I'll have to put some thought into that. For now, does it at least illustrate what I'm trying to do? |
Loosely, yes, altho as mentioned upthread, this sounds like a glorified event emitter which probably better exists in userland. |
Events are a natural part of the DOM, and by extension React. Signals and Slots is an implementation of the Observer pattern, without needing to implement the observer (the event emitter). The main motivation for using this over an event emitter is that both the sending and receiving components would need to have knowledge of the emitter. Lastly, My next syntax idea would be something like: const buttonRef = useRef(null);
const bannerRef = useRef(null);
connect(buttonRef, 'clicked', bannerRef, 'message');
<Button ref={buttonRef} />
<Banner ref={bannerRef} /> I will update the text of the RFC soon. |
Draft 2 has been added. Replaces the aliasing syntax with refs defined by Example above has been updated to reflect the latest changes. |
Can’t this be implemented with a import { useState } from 'react'
function Button({ name, emit }) {
const onClick = () => emit(`Hello, ${name}!`)
return <button onClick={onClick}>{name}</button>
}
function Banner({ message }) {
return <h1>{message}</h1>
}
function App() {
const [message, emit] = useState('')
return (
<>
<MyBanner message={message} />
<MyButton emit={emit} name="World" />
</>
)
} TS versionimport { useState } from 'react'
function Button({ name, emit }: { name: string, emit: (s: string) => void }) {
const onClick = () => emit(`Hello, ${name}!`)
return <button onClick={onClick}>{name}</button>
}
function Banner({ message }: { message: string }) {
return <h1>{message}</h1>
}
function App() {
const [message, emit] = useState('')
return (
<>
<MyBanner message={message} />
<MyButton emit={emit} name="World" />
</>
)
} |
@j-f1 Plus, It's still 'passing a function to a component as a parameter,' which is exactly what I am trying to avoid. Whenever you pass functions to a React component as a param, it inherently couples the child to its parent, because the parent is defining the child's behavior. For example, if the parent forgets to pass the emit function to the Button component, then the child's behavior will be broken. Signals and slots is meant to encourage separation of concerns. I admit that plenty of people have found usable alternatives to this, but I'm not sure if they would be scalable enough to built an entire production-level application out of them. |
The syntax now uses a `useSlot` hook to capture signals in the parent.
This is a major update. Instead of parents acting as an invisible mediator between two children, parents now capture the value of a signal emitted by a child which they can then pass to any other child. No more refs or keeping track of individual component instances. You can think of this as adding the observer pattern to React. But instead of notifying observers by calling one of their methods, they notify components of changes by using React component props, which automatically update whenever a value is updated. Most users have likely already implemented something very similar to the observer pattern by using e.g.: import React, { useSignal, useSlot, useState } from 'react';
import PropTypes from 'prop-types';
function TextInput ({ value, setValue }) {
const onChange = (e) => {
setValue(e.target.value);
};
return <input value={value} onChange={onChange}
}
TextInput.signals = {
value: PropTypes.string,
setValue: PropTypes.func
};
function Banner({ name }) {
return <h1>Hello, {name}!</h1>;
}
Banner.propTypes = {
name: PropTypes.string
};
function App () {
const [ name, setName ] = useState('');
return (
<>
<Banner name={name} />
<TextInput value={name} setValue={setName} />
</>
);
} Most users will almost certainly do something like this at some point. The top-level Compare this with the example listed at the top of the thread. Once again, this is something that can be done in user-space, but it doesn't fix the underlying problem. Each component should be able to isolate its own state from any other components. Another extra benefit: once the code for this is added to the React internals, it could be used to implement Error Boundaries as a hook. The observer pattern would work well for capturing an exception and then passing it up the stack until it reaches a handler. |
In the spirit of #182, I'll try to provide some quick thoughts rather than an exhaustive review. One thing that definitely can't work out is using strings for identifiers. We've learned that lesson with the legacy context API and don't want to repeat that mistake. In a larger app, strings always clash, so two components that are sufficiently far away (or just nested deeply) will at some point think that A minor nitpick is the
Yes, that's the canonical way to do this in React.
I don't agree with this assessment. From I agree there's still some question of just how much decoupling we want to convey through naming and patterns. One pattern we've wanted to encourage is putting a reducer's |
Somewhat tangential, but this is news to me. Is there an intention to deprecate |
There’s no immediate plan or anything like this but we’ve been de-emphasising it over the years, and much of the usage in the community has been replaced by TS. At some point it’s possible that we’ll stop “checking” prop types in React and people who want to use them would add |
This part is easy to avoid. The
I'll definitely need to put some more thought into this. With Qt C++, you pass in a method name ( If a signal is an event, then it doesn't need to travel all the way up the stack. It is only visible to the direct parent (i.e.
Then let me try another way of wording it: I want Here's a more condensed example of what I'm trying to do: import React, { useSignal, useSlot, useState } from 'react';
function TextInput () {
const [ value, setValue ] = useState('');
const emit = useSignal('updated');
const onChange = (e) => {
setValue(e.target.value);
emit(e.target.value);
};
return <input value={value} onChange={onChange} />
}
function App () {
const name = useSlot('updated');
return (
<>
<h1>Hello, {name}</h1>
<TextInput />
</>
);
} In this example, |
Closed in favor of my newer RFC: #246 |
Summary
This proposal is intended to introduce the concept of Signals and Slots used by Qt to React.
Signals and slots would be a natural, React-like way of enabling inter-component communication.
Simply passing messages between components shouldn't require passing callbacks that mutate
the parent's state or wrapping the Context API or a full-fledged state-management library like Redux.
It's backward compatible (signals can be connected even to old components).
Read the full RFC here.
Example