Skip to content
This repository has been archived by the owner on Jun 4, 2024. It is now read-only.

Custom hooks - March 1 #75

Merged
merged 29 commits into from
Feb 28, 2019
Merged

Custom hooks - March 1 #75

merged 29 commits into from
Feb 28, 2019

Conversation

valentijnnieman
Copy link
Contributor

@valentijnnieman valentijnnieman commented Sep 5, 2018

This adds customizable hooks before and after the fetch to dash-update-component, and makes the dash-renderer a standalone class called DashRenderer that can be called with an optional object holding request_pre and request_post functions, a la #65. Here's a simple example:

app.index_string = '''
<!DOCTYPE html>
<html>
    <head>
        {%metas%}
        <title>{%title%}</title>
        {%favicon%}
        {%css%}
    </head>
    <body>
        <div>Testing custom DashRenderer</div>
        {%app_entry%}
        <footer>
            {%config%}
            {%scripts%}
            <script id="_dash-renderer" type"application/json">
                const renderer = new DashRenderer({
                    request_pre: (req) => {
                        console.log('req:', req);
                        var output = document.getElementById('output-pre')
                        if(output) {
                            output.innerHTML = 'request_pre was fired and returned: <br/>req: ' + req + '<br/><br/>';
                        }
                    },
                    request_post: (req, res) => {
                        console.log('res:', res);
                        var output = document.getElementById('output-post')
                        if(output) {
                            output.innerHTML = 'request_post was fired and returned: <br/>res: ' + JSON.stringify(res) + '<br/><br/>';
                        }
                    }
                })
            </script>
        </footer>
        <div>With request hooks</div>
    </body>
</html>
'''

app.layout = html.Div([
    dcc.Input(
        id='input',
        value='initial value'
    ),
    html.Div(
        html.Div([
            html.Div(id='output-1'),
            html.Div(id='output-pre'),
            html.Div(id='output-post')
        ])
    )
])

@app.callback(Output('output-1', 'children'), [Input('input', 'value')])
def update_output(value):
    return value

It uses the new customizable index_string to setup a custom DashRenderer instance.

This depends on dash having support for a standalone dash-renderer, so this PR should be merged along side of this one.

src/DashRenderer.js Outdated Show resolved Hide resolved
src/DashRenderer.js Outdated Show resolved Hide resolved
src/actions/index.js Outdated Show resolved Hide resolved
src/actions/index.js Outdated Show resolved Hide resolved
src/actions/index.js Outdated Show resolved Hide resolved
Copy link
Member

@chriddyp chriddyp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just a few small changes

@valentijnnieman
Copy link
Contributor Author

@chriddyp Thanks for your detailed explanation! Had to write some logic for the case of hooks not being used, hope I got it right.

Copy link
Member

@chriddyp chriddyp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The storedHooks vs hooks feels a little convoluted to me and it seems like it's mostly just an artifact of our own data flow from config argument to getting something into the store.

Reading through this a second time, it seems like getting the data into the store is causing some complexity. Since this data is static (it doesn't change through the lifecycle of the app), I don't think that we really need to wire it into the store nor create our own actions.

Instead, could we just thread hooks down through to the actions? We're already threading it through the first few components (AppProvider ➡️ AppContainer ➡️ APIController), so let's just keep threading it through until we get to the place where we're firing the actions: ➡️ TreeContainer ➡️ NotifyObservers and then wire it in as arguments in the dispatch(notifyObservers()) API calls that the NotifyObservers component makes. (e.g. here:

dispatch(notifyObservers({id: ownProps.id, props: newProps}));
) and then include it in the input arguments of that function: https://github.com/plotly/dash-renderer/blob/master/src/actions/index.js#L180-L187. It's a lot of threading, but I think that's OK.

If we add more config arguments in the future, then we can abstract out hooks into a config argument and pass our data down this way.

The current solution of making these dispatches (and causing rerenders) inside APIController doesn't seem like it landed quite right, due to the complexity of hooks vs storedHooks and the fact that this data is static (it's not asynchronous like the rest of the API actions in APIController).

@chriddyp
Copy link
Member

Another solution might be to place it into the store when we create it, that is here:

const store = initializeStore();
const AppProvider = () => (
<Provider store={store}>
<AppContainer/>
. Perhaps store.initializationConfig = {hooks: hooks}. Or maybe there is another way to do this with the redux reducer initialization sequence. However, that seems a little less straightforward / standard than just wiring everything through.

@bpostlethwaite
Copy link
Member

bpostlethwaite commented Oct 10, 2018

FWIW I agree that wiring these hooks through the code is the way to go. Another approach for "global static config" is to use React context. However unless the config is used in a lot of places (unlike this case where we need it in a small number of sites) it's better to pass it down as props.

@valentijnnieman
Copy link
Contributor Author

@bpostlethwaite Yeah, isn't the context api part of React 16 though?

I'm also wondering: if I thread it all the way down, won't it fire for each component wrapped in a <NotifyObservers/> component? Will look into this more when I have a chance, @chriddyp sounds like you're right about it not having to be in the store though. Thanks!

@bpostlethwaite
Copy link
Member

Context has been part of the React API for a long time - it's recently been elevated out of "don't use this" status. But still it's probably best not to use it for the fact that it can make code harder to test / reason about.

@valentijnnieman
Copy link
Contributor Author

where we're firing the actions: ➡️ TreeContainer ➡️ NotifyObservers and then wire it in as arguments in the dispatch(notifyObservers()) API calls that the NotifyObservers component makes.

Won't that pass the hooks down to all components though? Feels like it shouldn't have to be passed down to each individual one, hence why I put it in the store.
What about passing the hooks to hydrateInitialOutputs and set them in the store from there, so that they're available each time updateOutput fires?

@valentijnnieman
Copy link
Contributor Author

@chriddyp Actually, thinking about it again, I don't think the dispatch(setHooks) call is async at all. It's not using any sort of thunk middleware, so I believe in this case the hooks will be set before anything else happens, so the previous version (without checking if the hooks are set) should be good.

Here's a stack overflow question about it

@chriddyp
Copy link
Member

Won't that pass the hooks down to all components though?

No it won't, it will only pass it down to the NotifyObservers component, which the user doesn't have access to. It won't be in the actual props of the component.

I'm also wondering: if I thread it all the way down, won't it fire for each component wrapped in a component?

I'm not sure what you mean by "fire". It won't cause any extra renders though, because it will just be integrated into the initial props of all of the parents.

so I believe in this case the hooks will be set before anything else happens

Can you verify this yourself by putting in debugger calls?

What about passing the hooks to hydrateInitialOutputs and set them in the store from there

The main thing that I'm concerned about is that the order of operations needs to be very clear. If we have this done in APIController, the dependency of setting hooks needing to be finished before updateOutput calls is implicit rather than explicit.

.circleci/config.yml Outdated Show resolved Hide resolved
src/actions/index.js Outdated Show resolved Hide resolved
tests/test_render.py Outdated Show resolved Hide resolved
tests/test_render.py Outdated Show resolved Hide resolved
src/DashRenderer.js Outdated Show resolved Hide resolved
Copy link
Collaborator

@alexcjohnson alexcjohnson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic @valentijnnieman - I believe we've reached the finish line! 💃

@alexcjohnson
Copy link
Collaborator

@chriddyp pretty sure all your comments from last fall have been long since addressed - can I dismiss your review?

@chriddyp
Copy link
Member

can I dismiss your review?

👍 💃 !

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants