Skip to content
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

initialize context.state sooner #1646

Open
dwhieb opened this issue Feb 16, 2022 · 15 comments · May be fixed by #1732
Open

initialize context.state sooner #1646

dwhieb opened this issue Feb 16, 2022 · 15 comments · May be fixed by #1732
Assignees
Milestone

Comments

@dwhieb
Copy link

dwhieb commented Feb 16, 2022

Expected Behavior

If there's information I want to be available in every request, I'd like to be able to specify that information when I initialize the app, using app.context.state, like so:

const app = new Koa()
app.context.state.prop = true // TypeError: Cannot set properties of undefined (setting 'prop')
app.use(ctx => console.log(ctx.state.prop))

Best practice for Koa is to use context.state to pass information through middleware, and to use app.context add properties used throughout the app, so this seems like the right approach. It's also parallel to the way that I can set app.locals in an Express app to make that information available to res.locals.

Actual Behavior

The above code snippet throws a TypeError because context.state isn't initialized until the app receives a request, here:

koa/lib/application.js

Lines 168 to 181 in aa816ca

createContext (req, res) {
const context = Object.create(this.context)
const request = context.request = Object.create(this.request)
const response = context.response = Object.create(this.response)
context.app = request.app = response.app = this
context.req = request.req = response.req = req
context.res = request.res = response.res = res
request.ctx = response.ctx = context
request.response = response
response.request = request
context.originalUrl = request.originalUrl = req.url
context.state = {}
return context
}

The following also doesn't work, because the state property gets overridden by the code linked above:

const app = new Koa()
app.context.state = { prop: true }
app.use(ctx => console.log(ctx.state.prop)) // returns undefined

Suggested Solution

Add the state property to the context prototype instead of initializing it when a request comes in. Then, in app.createContext(), create a new state object that only lives for the lifetime of the request, using app.context.state as its prototype.

Replace:

context.state = {}

with:

// context.state = Object.assign({}, context.state) // <- old suggestion
context.state = Object.create(context.state)        // <- this is probably better

Workaround

The workaround is to add the information directly to app.context rather than app.context.state. However, this is undesirable because it goes against the best practice of using context.state to encapsulate state.

const app = new Koa()
app.context.prop = true
app.use(ctx => console.log(ctx.prop)) // returns true

I'd be happy to open a PR for this if it's a change you'd like implemented.

@dwhieb dwhieb changed the title initialize state sooner initialize context.state sooner Feb 16, 2022
@3imed-jaberi
Copy link
Member

I like to see Set or Map here. It should behave better. @miwnwski ?!

@krisstern
Copy link

Hi, I would like to work on this issue.

@krisstern
Copy link

I am wondering if there is a development guide for contributors somewhere in the repo?

@krisstern krisstern linked a pull request Dec 24, 2022 that will close this issue
6 tasks
@iamgabrielsoft
Copy link

this has not being solved yet, i ran into this today

@dwhieb
Copy link
Author

dwhieb commented Jan 25, 2023

Reading my initial issue again, I'd actually recommend using Object.create() instead of Object.assign() for the change. I've edited the original comment to reflect this.

@krisstern
Copy link

@dwhieb The change does not work

@siakc
Copy link

siakc commented Dec 25, 2023

app.use((ctx, next)=> {ctx.state.sth = 'something'; await next();}
Put this before all MWs and you are good to go. A context has no meaning without a request. So the context can be valid in MWs chain only.

@jonathanong jonathanong added this to the v3.0.0 milestone Aug 29, 2024
@jonathanong
Copy link
Member

what's the use-case for this? I understand what you want, but I don't know why you want it.

@jonathanong
Copy link
Member

it's more likely going to cause issues because someone is going to not understand that a subset of the state is shared across requests

@dwhieb
Copy link
Author

dwhieb commented Aug 31, 2024

@jonathanong This is useful for any piece of data that the user would like available on all/most requests. This is standard behavior in other frameworks as well. See especially Express:

https://expressjs.com/en/5x/api.html#app.locals

@jonathanong
Copy link
Member

jonathanong commented Aug 31, 2024

you're saying what you want, not why you want it. just because others do it is not a good reason for us to do it too

the express examples are definitely bad - you don't want to add a module as a local. titles belong in your templating system (which Koa does not have, unlike Express). I have no idea why you'd have an email address as an app-level local

@dwhieb
Copy link
Author

dwhieb commented Aug 31, 2024

In general, having app-level context is useful whenever you need to read data from a file or database, but don't want to retrieve that data for every request. You can read the file/database once when the app initializes, rather than on every request.

Variables I've made available to all requests in the past include:

  • project metadata from package.json
  • SVG sprites
  • CSS/JS so I can inline it in a specific page
  • data from small databases stored as JSON files because the database isn't large enough to warrant an external database provider

@jonathanong
Copy link
Member

can you show some code example please? why would you attach it to the koa framework instead of using a singleton module?

@dwhieb
Copy link
Author

dwhieb commented Sep 2, 2024

No, the projects I was using Koa for have transitioned to Express. But here's a similar approach with Express:

https://github.com/dwhieb/Nisinoon/blob/main/app/locals.js

I'd attach it to the Koa framework's context object because the context object is the logical space that's been set aside for exactly those types of variables. Otherwise users have to remember which variables are stored in context and which are stored in the singleton.

Per Koa's documentation:

ctx.state

The recommended namespace for passing information through middleware and to your frontend views.

This strongly suggests that best practice is to store context variables here rather than in a separate singleton.

@koajs koajs deleted a comment from miwnwski Sep 16, 2024
@koajs koajs deleted a comment from miwnwski Sep 16, 2024
@kevinpeno
Copy link
Contributor

kevinpeno commented Oct 20, 2024

I'm still unsure why this should be in core, for a few reasons.

First, Koa has a documented philosophy that context (including state) is unique to each request.

A Context is created per request, and is referenced in middleware as the receiver, or the ctx identifier

Second, your initial suggestion to "update context" seems to be in line with the documentation as well, as it states the following and does not require a Koa update (emphasis mine):

app.context is the prototype from which ctx is created. You may add additional properties to ctx by editing app.context. This is useful for adding properties or methods to ctx to be used across your entire app, which may be more performant (no middleware) and/or easier (fewer require()s) at the expense of relying more on ctx, which could be considered an anti-pattern.

Also, note that that statement explicitly says you are opting into an anti-pattern by going about things the way described in this issue and in the associated PR.

Finally, context.state documents well how it should be interacted with through middleware to ensure state is how you want it in your frontend views.

ctx.state
The recommended namespace for passing information through middleware and to your frontend views.

So it seems to me that the best thing to do would be to either work with the establized patterns and create a framework that supports your application's per-request need for a globalized state via middleware or use the anti-pattern to throw the object directly on context.

I think @siakc's example perfectly expresses how to address your needs.

app.use((ctx, next)=> {ctx.state.sth = 'something'; await next();}

Where your middleware gets this global state from "somewhere" and merges it with the context state during request processing.

@kevinpeno kevinpeno self-assigned this Oct 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants