Skip to content

Commit 61319b1

Browse files
committed
Implement component instance hoisting to fix rerendering
Previously, child components were recreated on every render, causing state to reset and diff/patch to generate 0 patches since new instances had default values. This prevented counter updates from being reflected in the DOM. Changes: - Add childComponentInfo struct to track child component metadata - Add hoistedComponentMap to Generator for element-to-component mapping - Modify generateComponentStruct to add child component instance fields - Modify generateConstructor to: - Initialize hoistedVars map for proper reference generation - Create child component instances with their props - Modify generateBindAppMethod to propagate BindApp to child components - Modify generateRenderMethod to: - Populate hoistedComponentMap before rendering - Initialize hoistedVars for consistency - Modify generateElement to reference hoisted instances instead of creating new ones inline The generated App component now: - Has a counterInstance field persisting across renders - Initializes the counter instance once in NewApp - Calls BindApp on the counter instance - References c.counterInstance.Render() instead of creating new instances This ensures child component state persists and diff/patch correctly detects changes, allowing counter updates to display properly.
1 parent 0d35ebd commit 61319b1

2 files changed

Lines changed: 163 additions & 15 deletions

File tree

examples/counter/app_gen.go

Lines changed: 8 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/codegen/codegen.go

Lines changed: 155 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,22 @@ import (
1212
guixast "github.com/gaarutyunov/guix/pkg/ast"
1313
)
1414

15+
// childComponentInfo holds information about a child component instance
16+
type childComponentInfo struct {
17+
varName string
18+
componentType string
19+
element *guixast.Element // The element with props
20+
}
21+
1522
// Generator generates Go code from Guix components
1623
type Generator struct {
17-
fset *token.FileSet
18-
pkg string
19-
components map[string]bool // Track component names for this file
20-
hoistedVars map[string]bool // Track hoisted variable names in current component
21-
currentCompBody *guixast.Body // Current component body being generated
24+
fset *token.FileSet
25+
pkg string
26+
components map[string]bool // Track component names for this file
27+
hoistedVars map[string]bool // Track hoisted variable names in current component
28+
hoistedComponents []*childComponentInfo // Track hoisted child components
29+
hoistedComponentMap map[*guixast.Element]*childComponentInfo // Map elements to their hoisted component info
30+
currentCompBody *guixast.Body // Current component body being generated
2231
}
2332

2433
// New creates a new code generator
@@ -121,6 +130,9 @@ func (g *Generator) generateImports(file *guixast.File) *ast.GenDecl {
121130
func (g *Generator) generateComponent(comp *guixast.Component) []ast.Decl {
122131
var decls []ast.Decl
123132

133+
// Reset hoisted component map for this component
134+
g.hoistedComponentMap = make(map[*guixast.Element]*childComponentInfo)
135+
124136
// Generate Props struct if component has parameters
125137
if len(comp.Params) > 0 {
126138
decls = append(decls, g.generatePropsStruct(comp))
@@ -214,6 +226,39 @@ func (g *Generator) nodeHasTemplateInterpolation(node *guixast.Node) bool {
214226
return false
215227
}
216228

229+
// collectChildComponents recursively collects child component usages from nodes
230+
func (g *Generator) collectChildComponents(nodes []*guixast.Node) []*childComponentInfo {
231+
var childComponents []*childComponentInfo
232+
g.collectChildComponentsFromNodes(nodes, &childComponents)
233+
return childComponents
234+
}
235+
236+
func (g *Generator) collectChildComponentsFromNodes(nodes []*guixast.Node, result *[]*childComponentInfo) {
237+
for _, node := range nodes {
238+
if node.Element != nil {
239+
elem := node.Element
240+
// Check if this is a component (capitalized tag name)
241+
isComponent := g.components[elem.Tag] || (len(elem.Tag) > 0 && elem.Tag[0] >= 'A' && elem.Tag[0] <= 'Z' && !knownDOMElements[elem.Tag])
242+
243+
if isComponent {
244+
// Generate a variable name for this component instance
245+
varName := strings.ToLower(elem.Tag[:1]) + elem.Tag[1:] + "Instance"
246+
childInfo := &childComponentInfo{
247+
varName: varName,
248+
componentType: elem.Tag,
249+
element: elem,
250+
}
251+
*result = append(*result, childInfo)
252+
// Add to map for quick lookup during render generation
253+
g.hoistedComponentMap[elem] = childInfo
254+
}
255+
256+
// Recursively check children
257+
g.collectChildComponentsFromNodes(elem.Children, result)
258+
}
259+
}
260+
}
261+
217262
// generatePropsStruct generates a Props struct for component parameters
218263
func (g *Generator) generatePropsStruct(comp *guixast.Component) *ast.GenDecl {
219264
fields := make([]*ast.Field, len(comp.Params))
@@ -386,6 +431,18 @@ func (g *Generator) generateComponentStruct(comp *guixast.Component) *ast.GenDec
386431
}
387432
}
388433
}
434+
435+
// Add child component instance fields
436+
// Collect child components from the template
437+
childComponents := g.collectChildComponents(comp.Body.Children)
438+
for _, childInfo := range childComponents {
439+
fields = append(fields, &ast.Field{
440+
Names: []*ast.Ident{ast.NewIdent(childInfo.varName)},
441+
Type: &ast.StarExpr{
442+
X: ast.NewIdent(childInfo.componentType),
443+
},
444+
})
445+
}
389446
}
390447

391448
// Add listenersStarted flag if component has channel parameters
@@ -488,6 +545,16 @@ func (g *Generator) generateConstructor(comp *guixast.Component) *ast.FuncDecl {
488545

489546
// Initialize hoisted variables (like channels) in constructor
490547
if comp.Body != nil {
548+
// Set up hoisted vars map for expression generation
549+
g.hoistedVars = make(map[string]bool)
550+
g.currentCompBody = comp.Body
551+
for _, varDecl := range comp.Body.VarDecls {
552+
if len(varDecl.Names) == 1 && len(varDecl.Values) == 1 &&
553+
g.inferTypeFromExpr(varDecl.Values[0]) != nil {
554+
g.hoistedVars[varDecl.Names[0]] = true
555+
}
556+
}
557+
491558
for _, varDecl := range comp.Body.VarDecls {
492559
// Only initialize single-variable declarations with inferable types
493560
if len(varDecl.Names) == 1 && len(varDecl.Values) == 1 &&
@@ -503,6 +570,34 @@ func (g *Generator) generateConstructor(comp *guixast.Component) *ast.FuncDecl {
503570
})
504571
}
505572
}
573+
574+
// Initialize child component instances
575+
childComponents := g.collectChildComponents(comp.Body.Children)
576+
for _, childInfo := range childComponents {
577+
// Generate option calls from props
578+
optionCalls := []ast.Expr{}
579+
for _, prop := range childInfo.element.Props {
580+
optionCalls = append(optionCalls, &ast.CallExpr{
581+
Fun: ast.NewIdent(prop.Name),
582+
Args: []ast.Expr{g.generateExpr(prop.Value)},
583+
})
584+
}
585+
586+
// Generate: c.childInstance = NewChildComponent(options...)
587+
bodyStmts = append(bodyStmts, &ast.AssignStmt{
588+
Lhs: []ast.Expr{&ast.SelectorExpr{
589+
X: ast.NewIdent("c"),
590+
Sel: ast.NewIdent(childInfo.varName),
591+
}},
592+
Tok: token.ASSIGN,
593+
Rhs: []ast.Expr{
594+
&ast.CallExpr{
595+
Fun: ast.NewIdent("New" + childInfo.componentType),
596+
Args: optionCalls,
597+
},
598+
},
599+
})
600+
}
506601
}
507602

508603
bodyStmts = append(bodyStmts, &ast.ReturnStmt{
@@ -545,6 +640,11 @@ func (g *Generator) generateRenderMethod(comp *guixast.Component) *ast.FuncDecl
545640
}
546641
}
547642

643+
// Populate hoisted component map for this component
644+
if comp.Body != nil {
645+
g.collectChildComponents(comp.Body.Children)
646+
}
647+
548648
body := g.generateBody(comp.Body)
549649

550650
return &ast.FuncDecl{
@@ -794,7 +894,22 @@ func (g *Generator) generateElement(elem *guixast.Element) ast.Expr {
794894

795895
// Generate function call
796896
if isComponent {
797-
// Custom component: wrap in IIFE to call BindApp before Render
897+
// Check if this component is hoisted
898+
if hoistedInfo, isHoisted := g.hoistedComponentMap[elem]; isHoisted {
899+
// Hoisted component: just call Render on the hoisted instance
900+
// Generate: c.counterInstance.Render()
901+
return &ast.CallExpr{
902+
Fun: &ast.SelectorExpr{
903+
X: &ast.SelectorExpr{
904+
X: ast.NewIdent("c"),
905+
Sel: ast.NewIdent(hoistedInfo.varName),
906+
},
907+
Sel: ast.NewIdent("Render"),
908+
},
909+
}
910+
}
911+
912+
// Non-hoisted component: wrap in IIFE to call BindApp before Render
798913
// Generate:
799914
// func() *runtime.VNode {
800915
// comp := NewComponent(...)
@@ -1453,6 +1568,40 @@ func (g *Generator) generateBindAppMethod(comp *guixast.Component) *ast.FuncDecl
14531568
}
14541569
}
14551570

1571+
// Call BindApp on child component instances
1572+
if comp.Body != nil {
1573+
childComponents := g.collectChildComponents(comp.Body.Children)
1574+
for _, childInfo := range childComponents {
1575+
// Generate: if c.childInstance != nil { c.childInstance.BindApp(app) }
1576+
stmts = append(stmts, &ast.IfStmt{
1577+
Cond: &ast.BinaryExpr{
1578+
X: &ast.SelectorExpr{
1579+
X: ast.NewIdent("c"),
1580+
Sel: ast.NewIdent(childInfo.varName),
1581+
},
1582+
Op: token.NEQ,
1583+
Y: ast.NewIdent("nil"),
1584+
},
1585+
Body: &ast.BlockStmt{
1586+
List: []ast.Stmt{
1587+
&ast.ExprStmt{
1588+
X: &ast.CallExpr{
1589+
Fun: &ast.SelectorExpr{
1590+
X: &ast.SelectorExpr{
1591+
X: ast.NewIdent("c"),
1592+
Sel: ast.NewIdent(childInfo.varName),
1593+
},
1594+
Sel: ast.NewIdent("BindApp"),
1595+
},
1596+
Args: []ast.Expr{ast.NewIdent("app")},
1597+
},
1598+
},
1599+
},
1600+
},
1601+
})
1602+
}
1603+
}
1604+
14561605
// Set flag after starting listeners
14571606
if hasChannels {
14581607
stmts = append(stmts, &ast.AssignStmt{

0 commit comments

Comments
 (0)