Skip to content

Commit 00b1254

Browse files
authored
Merge pull request #1293 from reactjs/feature/add-option-to-replace-null-updated
Replace nulls in props from Rails with undefined
2 parents 95908b8 + edf889f commit 00b1254

File tree

6 files changed

+132
-4
lines changed

6 files changed

+132
-4
lines changed

.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,4 @@ Naming/RescuedExceptionsVariableName:
8686

8787
Metrics/BlockLength:
8888
Exclude:
89-
- 'test/**/*_test.rb'
89+
- 'test/**/*_test.rb'

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ Changes since last non-beta release.
99

1010
_Please add entries here for your pull requests that are not yet released._
1111

12+
#### Added
13+
- Added option to replace `null`s in props with `undefined` via `config.react.null_to_undefined_props` in `config/application.rb` #1293
14+
1215
## [3.0.0] - 2023-08-14
1316

1417
### Breaking Changes

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,16 @@ Read the [full review here](https://clutch.co/profile/shakacode#reviews?sort_by=
6868
- [Upgrading](#upgrading)
6969
- [2.7 to 3.0](#27-to-30)
7070
- [2.3 to 2.4](#23-to-24)
71+
- [Other features](#other-features)
72+
- [Replace `null` with `undefined` in props](#replace-null-with-undefined-in-props)
7173
- [Common Errors](#common-errors)
7274
- [Getting warning for `Can't resolve 'react-dom/client'` in React < 18](#getting-warning-for-cant-resolve-react-domclient-in-react--18)
7375
- [Undefined Set](#undefined-set)
7476
- [Using TheRubyRacer](#using-therubyracer)
7577
- [HMR](#hmr)
7678
- [Related Projects](#related-projects)
7779
- [Contributing](#contributing)
80+
- [Supporters](#supporters)
7881

7982
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
8083

@@ -528,7 +531,6 @@ use it like so:
528531
ReactUJS.getConstructor = ReactUJS.constructorFromRequireContext(require.context('components', true));
529532
```
530533

531-
532534
## Server-Side Rendering
533535

534536
You can render React components inside your Rails server with `prerender: true`:
@@ -801,6 +803,26 @@ For the vast majority of cases this will get you most of the migration:
801803
- add `import PropTypes from 'prop-types'` (Webpacker only)
802804
- re-run `bundle exec rails webpacker:install:react` to update npm packages (Webpacker only)
803805

806+
## Other features
807+
808+
### Replace `null` with `undefined` in props
809+
810+
React-Rails converts `nil` to `null` while parsing props from Ruby to JavaScript. Optionally, you can configure React-Rails to parse `nil` values to `undefined` as per the following:
811+
812+
```ruby
813+
# config/application.rb
814+
module TheAppName
815+
class Application < Rails::Application
816+
# ...
817+
# Set to true to convert null values in props into undefined
818+
config.react.null_to_undefined_props = true
819+
# ...
820+
end
821+
end
822+
```
823+
824+
More information in: [discussion#1272](https://github.com/reactjs/react-rails/discussions/1272).
825+
804826
## Common Errors
805827
### Getting warning for `Can't resolve 'react-dom/client'` in React < 18
806828

@@ -857,7 +879,7 @@ By contributing to React-Rails, you agree to abide by the [code of conduct](http
857879

858880
You can always help by submitting patches or triaging issues. Even offering reproduction steps to issues is incredibly helpful!
859881

860-
# Supporters
882+
## Supporters
861883

862884
The following companies support the development of this and other open-source projects maintained by ShakaCode by providing licenses to the ShakaCode team. ShakaCode stands by the usefulness of these products!
863885

lib/react/rails/component_mount.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ def generate_html_options(name, options, props, prerender_options)
5656
unless prerender_options == :static
5757
html_options[:data].tap do |data|
5858
data[:react_class] = name
59-
data[:react_props] = (props.is_a?(String) ? props : props.to_json)
59+
data[:react_props] = props_to_json(
60+
props,
61+
null_to_undefined: Dummy::Application.config.react.null_to_undefined_props
62+
)
6063
data[:hydrate] = "t" if prerender_options
6164

6265
num_components = @cache_ids.count { |c| c.start_with? name }
@@ -67,6 +70,17 @@ def generate_html_options(name, options, props, prerender_options)
6770
html_options
6871
end
6972

73+
def props_to_json(props, options = { null_to_undefined: false })
74+
return props if props.is_a?(String)
75+
return props.to_json unless options[:null_to_undefined]
76+
77+
# This regex matches key:value with null values while ensuing no string with similar
78+
# pattern gets matched. It doesn't include null values in arrays.
79+
props.to_json
80+
.gsub(/([^\\]":)null([,}\]])/, '\1undefined\2') # match simple null values
81+
.gsub(/([^\\]":(\[[^\\"]+,|\[))null([,\]])/, '\1undefined\3') # Match nulls in array
82+
end
83+
7084
def rendered_tag(html_options, &block)
7185
html_tag = html_options[:tag] || :div
7286

lib/react/rails/railtie.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class Railtie < ::Rails::Railtie
1212
config.react.jsx_transformer_class = nil # defaults to BabelTransformer
1313
config.react.camelize_props = false # pass in an underscored hash but get a camelized hash
1414
config.react.sprockets_strategy = nil # how to attach JSX to the asset pipeline (or `false` for none)
15+
config.react.null_to_undefined_props = false # Set to true to convert null values in props into undefined
1516

1617
# Server rendering:
1718
config.react.server_renderer_pool_size = 1 # increase if you're on JRuby

test/react/rails/component_mount_test.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require "test_helper"
44

5+
# rubocop:disable Metrics/ClassLength
56
class ComponentMountTest < ActionDispatch::IntegrationTest
67
module DummyRenderer
78
def self.render(component_name, props, _prerender_options)
@@ -128,5 +129,92 @@ def self.react_rails_prerenderer
128129

129130
assert_equal %(<div>rendered Foo with {&quot;ok&quot;:true}</div>), rendered_component
130131
end
132+
133+
test "#react_component sets null props to undefined when null_to_undefined_props set to true" do
134+
app.config.react.null_to_undefined_props = true
135+
136+
@helper.setup(DummyController)
137+
rendered_component = @helper.react_component("Foo", { bar: nil, content: 'bar":null,' })
138+
139+
assert_includes rendered_component, '&quot;bar&quot;:undefined,&quot;content&quot;:&quot;bar\\&quot;:null,&quot;'
140+
end
141+
142+
test "#react_component passes null props as null when null_to_undefined_props set to false" do
143+
app.config.react.null_to_undefined_props = false
144+
145+
@helper.setup(DummyController)
146+
rendered_component = @helper.react_component("Foo", { bar: nil, content: 'bar":null,' })
147+
148+
assert_includes rendered_component, "&quot;bar&quot;:null,&quot;content&quot;:&quot;bar\\&quot;:null,&quot;"
149+
end
150+
151+
test "#props_to_json doesn't converts null values to undefined be default" do
152+
props = { name: nil }
153+
expected_json = '{"name":null}'
154+
component_mount = React::Rails::ComponentMount.new
155+
156+
actual_json = component_mount.send(:props_to_json, props)
157+
158+
assert_equal(expected_json, actual_json)
159+
end
160+
161+
test "#props_to_json converts null values to undefined with null_to_undefined: true option" do
162+
props = { bar: nil, content: 'bar":null,' }
163+
expected_json = '{"bar":undefined,"content":"bar\\":null,"}'
164+
component_mount = React::Rails::ComponentMount.new
165+
166+
actual_json = component_mount.send(:props_to_json, props, { null_to_undefined: true })
167+
168+
assert_equal(expected_json, actual_json)
169+
end
170+
171+
test "#props_to_json converts null values in arrays to undefined with null_to_undefined: true option" do
172+
props = { items1: [nil], items2: [1, nil], items3: [nil, 1], items4: [1, nil, 2] }
173+
expected_json = '{"items1":[undefined],"items2":[1,undefined],"items3":[undefined,1],"items4":[1,undefined,2]}'
174+
component_mount = React::Rails::ComponentMount.new
175+
176+
actual_json = component_mount.send(:props_to_json, props, { null_to_undefined: true })
177+
178+
assert_equal(expected_json, actual_json)
179+
end
180+
181+
test "#props_to_json doesnt converts null-like values in arrays to undefined with null_to_undefined: true option" do
182+
props = {
183+
items1: "[null]",
184+
items2: "[1,null]",
185+
items3: "[null,1]",
186+
items4: "[1,null,2]",
187+
items5: '["a",null]',
188+
items6: '[null,"b"]',
189+
items7: '["a",null,"b"]',
190+
items8: '["a",nullx,"b"]'
191+
}
192+
expected_json = '{"items1":"[null]","items2":"[1,null]","items3":"[null,1]","items4":"[1,null,2]",' \
193+
'"items5":"[\"a\",null]","items6":"[null,\"b\"]","items7":"[\"a\",null,\"b\"]"' \
194+
',"items8":"[\"a\",nullx,\"b\"]"}'
195+
component_mount = React::Rails::ComponentMount.new
196+
197+
actual_json = component_mount.send(:props_to_json, props, { null_to_undefined: true })
198+
199+
assert_equal(expected_json, actual_json)
200+
end
201+
202+
test "#props_to_json doesnt converts null values in nested arrays to undefined with null_to_undefined: true" do
203+
props = {
204+
items1: nil,
205+
items2: [1, nil, 2],
206+
items3: nil,
207+
items4: "[1, null, 2]",
208+
items5: nil
209+
}
210+
expected_json = '{"items1":undefined,"items2":[1,undefined,2],"items3":undefined,"items4":"[1, null, 2]"' \
211+
',"items5":undefined}'
212+
component_mount = React::Rails::ComponentMount.new
213+
214+
actual_json = component_mount.send(:props_to_json, props, { null_to_undefined: true })
215+
216+
assert_equal(expected_json, actual_json)
217+
end
131218
end
132219
end
220+
# rubocop:enable Metrics/ClassLength

0 commit comments

Comments
 (0)